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
| Pattern | Behavior | Use When |
|---|---|---|
| Fail-fast (original) | First error aborts all | Operations are atomic (all-or-nothing) |
| Collect-all (fix) | Process all, accumulate errors | Entries are independent, partial success is acceptable |
| Best-effort + report | Process all, return result + errors | APIs where caller decides on partial results |
The collect-all pattern is the right choice here because:
- Each override is independent — validity of one doesn’t depend on another
- Users expect partial application (valid overrides should work even if one is broken)
- 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.