Skip to content

fix: override_if_not_empty should not skip falsy primitives (0, False, 0.0) - #41

Merged
toumorokoshi merged 3 commits into
toumorokoshi:masterfrom
gaoflow:fix-override-if-not-empty-falsy
Aug 17, 2026
Merged

fix: override_if_not_empty should not skip falsy primitives (0, False, 0.0)#41
toumorokoshi merged 3 commits into
toumorokoshi:masterfrom
gaoflow:fix-override-if-not-empty-falsy

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Problem

TypeConflictStrategies.strategy_override_if_not_empty uses a bare truthiness check:

return nxt if nxt else base

This treats any falsy value as "empty", including 0, False, and 0.0.
These are valid, intentional values — they are not empty, and they are not
null — so they should override base.

from deepmerge import Merger
m = Merger([], "override", "override_if_not_empty")

m.merge("base", 0)      # returns "base"  ← wrong, should be 0
m.merge("base", False)  # returns "base"  ← wrong, should be False
m.merge("base", 0.0)    # returns "base"  ← wrong, should be 0.0

Fix

Replace the truthiness check with an explicit null/empty test:

  1. None → keep base (null)
  2. len(nxt) == 0 (dict, list, set, str, …) → keep base (empty container)
  3. Anything else (including 0, False, 0.0) → return nxt

The empty check is isinstance(nxt, Sized) and len(nxt) == 0, so non-sized
types (ints, floats, bools) fall through to the override path.

The existing tests pass unchanged; a parameterized test covers the falsy
primitives and the empty/null cases.

gaoflow added 2 commits June 24, 2026 16:00
…, 0.0)

The previous implementation used `nxt if nxt else base`, which treats any
falsy value — including `0`, `False`, and `0.0` — as "empty" and silently
keeps `base`.  These are valid non-empty values that should override `base`.

Only `None` (null) and sized empty containers (len == 0) qualify as
"empty or null" per the strategy's documented contract.  Falsy primitives
have no `len()` and are neither null nor empty, so they now correctly
override `base`.
Comment thread deepmerge/tests/strategy/test_type_conflict.py Outdated

@toumorokoshi toumorokoshi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! this looks right, but a quick comment.

Also although I agree this is a "fix", I think we'll do a major version bump - it's a breaking change to a merge strategy.

@gaoflow

gaoflow commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Parameterized in e16063a — 13 cases with ids, covering the falsy primitives plus the empty/null side so both directions are in one table. Your point about the early exit is exactly what shows up: against master the run reports all four broken cases instead of stopping at the first one.

FAILED test_merge_if_not_empty_falsy[zero int]
FAILED test_merge_if_not_empty_falsy[false]
FAILED test_merge_if_not_empty_falsy[zero float]
FAILED test_merge_if_not_empty_falsy[zero complex]
4 failed, 9 passed

It asserts the type as well as the value, since 0 == False would otherwise let a wrong return slip through. Whole suite is 34 passed, black --check and mypy clean.

Major version bump is your call, that makes sense to me. Also corrected the PR description: it still claimed a try/except TypeError, which 07e8b4f replaced with the Sized check.

@toumorokoshi

Copy link
Copy Markdown
Owner

great, thank you!

@toumorokoshi
toumorokoshi merged commit d180db5 into toumorokoshi:master Aug 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants