Merge and compare dictionaries¶
The question. You have two dictionaries — defaults and overrides, or two snapshots of the same config — and you want to combine them (with one winning on conflicts), or diff them to see what's the same, what's different, and what's unique to each side.
Merging has one canonical form: defaults | overrides. Comparing uses the set-operation support on .keys() views. Both fall out of the language once you know where to look.
# Merge: right-hand side wins on conflicts
defaults = {'theme': 'light', 'language': 'en-GB', 'font_size': 14, 'show_line_numbers': True}
user = {'theme': 'dark', 'font_size': 16}
config = defaults | user
print('merged:', config)
# In-place merge — mutates the left-hand side
defaults |= user
print('in-place:', defaults)
# Dictionary keys support set operations — use them to compare
dict_a = {'name': 'Alice', 'age': 30, 'city': 'London'}
dict_b = {'name': 'Alice', 'age': 25, 'email': 'alice@example.com'}
common = dict_a.keys() & dict_b.keys()
only_a = dict_a.keys() - dict_b.keys()
only_b = dict_b.keys() - dict_a.keys()
print('common:', common)
print('only in A:', only_a)
print('only in B:', only_b)
# For shared keys, compare the values
for key in common:
if dict_a[key] == dict_b[key]:
print(f'{key}: same ({dict_a[key]})')
else:
print(f'{key}: different (A={dict_a[key]}, B={dict_b[key]})')
Why it works¶
| and |= on dicts (Python 3.9+) read as "union with right-side precedence". For one-level merges, they're the cleanest option — you don't have to remember whether the older {**a, **b} syntax merges or whether a.update(b) returns the result or None (it returns None — a common trip-up).
The comparison trick relies on a less-obvious property of dicts: .keys() returns a view that behaves like a set. a.keys() & b.keys(), a.keys() - b.keys(), and a.keys() | b.keys() all work directly — no need to wrap them in set(...). The views are live and cheap; they don't copy the keys.
For value comparison, once you have the common-keys set you loop once over it and check each value — O(n) in the smaller dict.
Trade-offs¶
| is shallow. {'user': {'name': 'A'}} | {'user': {'age': 30}} gives you {'user': {'age': 30}} — the nested name is gone. For deep merges you need to recurse yourself, or reach for a library like deepmerge.
update() is the old way. It still works in every Python 3, mutates in place, and returns None. If you're on 3.9 or newer, prefer |= — same effect, cleaner reading, doesn't look like a function that forgot to return its result.
**{**a, **b} is equivalent to a | b** on Python 3.9+, but the pipe version is easier to read in a chain (a | b | c). The dict-unpacking form is still the right call when one of the sides is literal — {**base, 'count': 0} reads better than base | {'count': 0}.
Comparing values across different dicts is only as good as the types allow. If values contain dicts, lists, or custom classes, == follows their __eq__. For floats, use math.isclose instead.
Related reading¶
- Convert between data structures — the broader "reshape your data" toolkit.
- Work with nested structures — when shallow merges stop being enough.
- Dictionary methods — every
dictmethod at a glance.