Fix: apply_overwrites_to_context silently drops overrides after first invalid entry

How cookiecutter/cookiecutter#2219 fixed silent data loss in context generation — why batch validation should collect all errors, not fail on the first.

The Bug

Repo: cookiecutter/cookiecutter Issue: #2219 Status: PR-submitted PR: https://github.com/cookiecutter/cookiecutter/pull/2230

apply_overwrites_to_context validated user-provided overrides against the cookiecutter template schema. If an override failed validation (e.g., wrong type), it raised ValueError immediately. The caller caught this error and warned, but silently dropped all remaining overrides — not just the invalid one.

If a user had 5 overrides and the 2nd one was invalid, overrides 3, 4, and 5 were silently discarded with only a single warning about “Invalid default received.”

Root Cause

The original code wrapped the entire batch in a single try/except:

try:
    apply_overwrites_to_context(obj, default_context)
except ValueError as error:
    warnings.warn(f"Invalid default received: {error}")

This is a fail-fast pattern: the first error aborts the entire batch. For user-provided input with multiple independent entries, fail-fast causes data loss.

The Fix

@@ -161,10 +161,16 @@ def generate_context(
     # Overwrite context variable defaults with the default context from the
     # user's global config, if available
     if default_context:
-        try:
-            apply_overwrites_to_context(obj, default_context)
-        except ValueError as error:
-            warnings.warn(f"Invalid default received: {error}")
+        errors: list[str] = []
+        for key, value in default_context.items():
+            try:
+                apply_overwrites_to_context(obj, {key: value})
+            except ValueError as error:
+                errors.append(str(error))
+        if errors:
+            warnings.warn(
+                f"Invalid default(s) received: {'; '.join(errors)}"
+            )
     if extra_context:
         apply_overwrites_to_context(obj, extra_context)

The fix switches from batch to per-entry validation, collecting all errors into a list. Each override is tried independently — invalid ones are skipped with an error message, while valid ones still apply.

Error Handling Patterns: Fail-Fast vs. Collect-All

PatternBehaviorUse When
Fail-fast (original)First error aborts allOperations are atomic (all-or-nothing)
Collect-all (fix)Process all, accumulate errorsEntries are independent, partial success is acceptable
Best-effort + reportProcess all, return result + errorsAPIs where caller decides on partial results

The collect-all pattern is the right choice here because:

  1. Each override is independent — validity of one doesn’t depend on another
  2. Users expect partial application (valid overrides should work even if one is broken)
  3. Silent data loss is worse than noisy failure

Test Update

-    with pytest.warns(UserWarning, match="Invalid default received"):
+    with pytest.warns(UserWarning, match="default\\(s\\) received"):

The test regex was updated to match the new plural message format. Note the escaped parentheses — the warning message now says “default(s) received” with literal parentheses.

The Wording Change

The message changed from “Invalid default received” (singular) to “Invalid default(s) received” (plural-aware). The (s) convention is a common English shorthand for “singular or plural” without requiring the code to handle both forms separately. It reads naturally for both cases:

  • 1 error: “Invalid default(s) received: type mismatch for ‘name’”
  • 3 errors: “Invalid default(s) received: type mismatch for ‘name’; bad type for ‘age’; unknown key ‘foo‘“

Transfer Pattern

The batch validation audit pattern: when processing a collection of independent inputs, never wrap the entire loop in a single try/except. Instead, validate each entry individually and collect errors. This prevents one bad entry from silently discarding good ones.


Auto-generated from PR #2219. View all patches on GitHub.