Deep merge dictionaries of dictionaries in Python
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Deep merging combines nested dictionaries while preserving existing branches instead of replacing entire sections at the top level. This is common in layered configuration systems where environment overrides should change only selected values. A good deep-merge function should be explicit about conflict policy, avoid accidental mutation, and be easy to test.
Shallow Merge vs Deep Merge
A shallow merge replaces values only by top-level keys. That can silently remove nested data.
In this result, the nested db dictionary from base is replaced entirely. A deep merge should keep unchanged nested keys and only override requested parts.
Basic Recursive Deep Merge
A common strategy is recursion when both values are dictionaries.
This creates a new dictionary and leaves inputs unchanged.
Define Conflict Policy Explicitly
Deep merge behavior is not universal. Teams should decide how to handle lists, None, and type mismatches.
Example policy choices:
- when both values are lists: replace, append, or append unique
- when incoming value is
None: keep existing or treat as explicit clear - when types differ: override or raise an error
A configurable helper keeps these decisions explicit.
Keeping this policy in one helper avoids hidden behavior differences across services.
Mutation and Copy Strategy
Some deep-merge snippets mutate the base dictionary directly. That can be fine for controlled scripts but risky in request handlers or shared caches. Non-mutating merges are usually easier to reason about because callers can compare original and merged states safely.
If performance is a concern, profile before switching to in-place mutation. For most config workloads, clarity and correctness matter more than micro-optimization.
Testing Deep Merge Logic
Merge helpers look simple but fail in edge cases. Add tests for:
- nested dictionary override
- list policy behavior
- type mismatches
- empty dictionaries
- immutability of inputs
Example quick tests:
These checks catch regressions when policy logic evolves.
Large Payload Considerations
For large nested payloads, repeated deep merges can be expensive. Practical optimizations:
- merge shared defaults once at startup
- cache merged profiles when input combinations repeat
- avoid unnecessary merging in tight loops
Do not optimize blindly. Measure actual merge cost in realistic workloads first.
Common Pitfalls
- Using shallow merge operators and assuming nested keys are preserved.
- Mutating original dictionaries unintentionally.
- Leaving list behavior undefined across modules.
- Ignoring type mismatches that should trigger explicit errors.
- Skipping tests because recursive merge function looks small.
Summary
- Deep merge preserves nested branches while applying targeted overrides.
- Recurse only when both values are dictionary-like objects.
- Document and implement explicit conflict policies for lists and mismatched types.
- Prefer non-mutating merge results for safer reasoning.
- Add focused tests to keep merge behavior stable over time.

