How to merge two dicts.
The | operator. Or the unpacking. Or update.
Three ways to merge dictionaries in Python, each appropriate in a different situation. The | operator (Python 3.9+) creates a new dict, {'**d1, **d2'} does the same and works on older versions, and .update() mutates in place. The precedence rule is consistent: the right side wins on conflicting keys. This page covers each method, the difference between | and |=, and when ChainMap beats all of them.
Short answer: Use d1 | d2 (Python 3.9+) — creates a new dict, d2's keys win. Use {'**d1, **d2'} for compatibility with older Python — same behavior. Use d1.update(d2) to modify d1 in place (returns None). The right side wins on conflict. |= mutates in place like .update(). ChainMap references the original dicts without copying — good for layered config. All built-in methods are shallow.
✅ The four methods
Each with a clear use case.
d1 = {'a': 1, 'b': 2}
d2 = {'b': 99, 'c': 3}
# Method 1: | operator (Python 3.9+)
merged = d1 | d2
print(merged) # {'a': 1, 'b': 99, 'c': 3}
print(d1) # {'a': 1, 'b': 2} — unchanged
print(d2) # {'b': 99, 'c': 3} — unchanged
# Method 2: {**d1, **d2} — works on all Python 3
merged = {**d1, **d2}
print(merged) # {'a': 1, 'b': 99, 'c': 3}
# Method 3: .update() — mutates in place
d3 = d1.copy()
d3.update(d2)
print(d3) # {'a': 1, 'b': 99, 'c': 3}
print(d1) # {'a': 1, 'b': 2} — unchanged (used copy)
# Method 4: |= — mutates in place (Python 3.9+)
d4 = d1.copy()
d4 |= d2
print(d4) # {'a': 1, 'b': 99, 'c': 3}
# All four produce the same merged dict.
# The difference is mutation vs creation.
Precedence rule (all methods):
d1 = {'b': 2}
d2 = {'b': 99}
d1 | d2 → {'b': 99} (right wins)
{**d1, **d2} → {'b': 99} (right wins)
d1.update(d2) → d1 is {'b': 99} (right wins)
To make the left win, reverse:
d2 | d1 → {'b': 2}
{**d2, **d1} → {'b': 2}
The rule is consistent: last one wins.
Same as function argument order, or CSS cascade.
Which to use:
✅ Python 3.9+, creating a new dict → use |
Clearest. Returns a new dict.
✅ Python < 3.9, creating a new dict → use {**d1, **d2}
Same behavior, just older syntax.
✅ Modifying an existing dict → use .update()
Returns None. Modifies in place.
✅ Python 3.9+, modifying an existing dict → use |=
Same as .update(), shorter.
❌ Merging more than two dicts with .update()
Requires a loop or chain.
❌ Merging without copying, when you need
a read-only view → use ChainMap.
The pitfall:
d1 = {'a': 1}
d2 = {'b': 2}
d1.update(d2)
# d1 is now {'a': 1, 'b': 2}
# If another part of the code has a reference
# to d1, that reference now sees the merged dict.
# Use | or {**} to avoid this:
merged = d1 | d2
# d1 unchanged. merged is new.
The default should be:
| for new dict, .update() for in-place mutation.
Choose based on whether other code holds a reference.
✅ | vs |= — the mutation difference
Same operator, different semantics.
# | creates a new dict
d1 = {'a': 1}
d2 = {'b': 2}
merged = d1 | d2
print(merged) # {'a': 1, 'b': 2}
print(d1) # {'a': 1} — unchanged
print(id(d1) == id(merged)) # False — different objects
# |= mutates in place
d1 = {'a': 1}
d2 = {'b': 2}
original_id = id(d1)
d1 |= d2
print(d1) # {'a': 1, 'b': 2}
print(id(d1) == original_id) # True — same object
# The reference difference
def bad_update(config):
config |= {'debug': True}
return config
def good_update(config):
return config | {'debug': True}
my_config = {'name': 'app'}
# good_update doesn't modify my_config
new_config = good_update(my_config)
print(my_config) # {'name': 'app'}
print(new_config) # {'name': 'app', 'debug': True}
# bad_update modifies my_config
bad_update(my_config)
print(my_config) # {'name': 'app', 'debug': True} ← modified!
The difference:
| → creates a new dict
The left operand is unchanged.
The right operand is unchanged.
Returns a third, new dict.
|= → mutates the left operand
The left operand is updated in place.
The right operand is unchanged.
Returns the left operand (same object).
When to use which:
✅ | — when the caller shouldn't see changes
Function returns a merged dict without
modifying the input.
def merge_config(base, overrides):
return base | overrides
✅ |= — when the caller expects changes
In-place update of a settings dict.
def apply_overrides(config, overrides):
config |= overrides
# config is now updated, caller sees it
Same for += vs + on lists, sets, and strings.
The augmented assignment mutates; the plain
operator creates a new object.
The rule of thumb:
If your function accepts a dict argument
and returns a merged version, use |.
If your function accepts a dict argument
and modifies it in place (like a settings
object, cache, or accumulator), use |=.
The choice signals intent to the caller.
⚠️ Don't mix them:
❌ def bad(x):
x |= something
return x
# Modifies the caller's dict AND returns it.
# Confusing. Pick one behavior.
✅ def good(x):
return x | something
# Pure function. No mutation.
✅ def update_in_place(x):
x |= something
# no return (or return None)
# Clear it's an in-place operation.
The function signature (return value, or
None) should match the behavior (new dict,
or in-place).
✅ ChainMap — the no-copy option
When you don't want to merge.
from collections import ChainMap
defaults = {'color': 'blue', 'size': 10}
user = {'color': 'red'}
overrides = {'size': 20}
# ChainMap — a view over multiple dicts
config = ChainMap(overrides, user, defaults)
# Lookups check overrides first, then user, then defaults.
print(config['color']) # 'red' (from user)
print(config['size']) # 20 (from overrides)
# Under the hood: no copying.
# The three dicts are the SAME objects.
# If they change, ChainMap sees the change.
user['color'] = 'green'
print(config['color']) # 'green'
# In contrast, a merged dict is a snapshot:
merged = defaults | user | overrides
user['color'] = 'yellow'
print(merged['color']) # 'red' — still the old value
print(config['color']) # 'yellow' — ChainMap sees the change
# ChainMap is read-mostly:
# Writes go to the first dict.
config['size'] = 30
print(overrides) # {'size': 30} — modified!
print(user) # unchanged
print(defaults) # unchanged
# To write to a specific dict:
# chains.new_child() or chains.maps[0]['size'] = 30
ChainMap vs merged dict:
ChainMap:
→ no copy
→ lookups check each dict in order
→ reflects live changes
→ writes go to first dict
→ good for layered config
Merged dict (|, {**}, .update):
→ copies all key-value pairs
→ lookups check the one dict
→ frozen snapshot
→ writes modify the merged dict
→ good for a standalone dict
When to use ChainMap:
✅ Layered configuration:
defaults + user + env overrides
→ no need to copy defaults into user
✅ Temporary views:
"show me the current config"
→ reflects changes without re-merging
✅ Read-heavy code:
If you look up keys many times, ChainMap's
no-copy is faster than merging once.
✅ Large dicts:
For a 1 MB dict, merging copies 1 MB.
ChainMap references, no copy.
When NOT to use ChainMap:
❌ You need a standalone dict.
ChainMap is a view, not a dict.
Pass it to code that expects a dict,
and it may not work.
❌ You need to write to specific dicts.
Writes go to the first dict only.
To write to a different dict, use .maps[i].
❌ You need performance on reads.
Actually — ChainMap is fast on reads.
But it's slower than a single dict,
because each miss checks the next dict.
For a small number of dicts, no problem.
For hundreds, use a merged dict.
The common ChainMap pattern:
from collections import ChainMap
def get_config():
defaults = {'debug': False, 'port': 8080}
user = load_user_config() or {}
env = {
k: v for k, v in os.environ.items()
if k.startswith('APP_')
}
return ChainMap(env, user, defaults)
config = get_config()
config['port'] # env wins, then user, then defaults
# All three dicts are visible.
# Changes to env/user/defaults are reflected.
# Reads are O(len(dicts)), not O(1).
# Fine for 3-5 layers of config.
For most merging tasks, use |.
ChainMap is for the specific pattern of
"layered lookups without copying."
If you don't need that, don't use it.
Don't use | before Python 3.9. The dict merge operator | was added in Python 3.9 (PEP 584). On Python 3.8 or earlier, d1 | d2 raises TypeError: unsupported operand type(s) for |: 'dict' and 'dict'. If you need to support older Python, use {**d1, **d2} — same behavior, works on all Python 3 versions (the ** unpacking syntax was added in Python 3.5). Similarly, |= for dicts is Python 3.9+. In modern code (3.9+), use |. In code that needs to run on older versions, use {**d1, **d2}.
⚠️ Common mistakes
The dict merge mistakes that cost real time.
| on Python 3.8TypeError — the operator doesn't exist. Fix: {'**d1, **d2'}.
.update() to return a dictIt returns None. Fix: use | if you need a return value.
| and |=| creates a new dict; |= mutates. Fix: use | for new dicts, |= for in-place.
Nested dicts are replaced, not merged. Fix: use mergedeep or explicit recursion.
The right dict wins on conflict. Fix: reverse the order if you want the left to win.
ChainMap when a dict is expectedChainMap is a view, not a dict. Fix: convert with dict(chainmap) if needed.
Confusing side effects. Fix: make a copy first, or use |.
+Lists support +, dicts don't. Fix: use |, {**d1, **d2}, or .update().
❓ Frequently asked
How do I merge two dictionaries in Python?
Three methods, each for a different situation. (1) Python 3.9+: use the | operator — merged = d1 | d2 — creates a new dict, d2's keys win on collision. (2) Any Python 3: use {**d1, **d2} — same behavior, works everywhere. (3) Use d1.update(d2) — mutates d1 in place, returns None. In modern code, use |.
What happens when the dictionaries have overlapping keys?
The later dictionary wins. In d1 | d2, keys from d2 overwrite keys from d1. Same for {**d1, **d2} and d1.update(d2). If you want d1 to win, reverse the order: d2 | d1. The precedence rule is simple: right side wins.
What is the difference between | and |=?
| creates a new dict: merged = d1 | d2 — d1 and d2 are unchanged. |= mutates in place: d1 |= d2 — d1 is updated with d2's keys, d2 unchanged, and d1 is the same object. The difference matters when other code holds a reference to d1.
What is ChainMap and when should I use it?
ChainMap is a class in the collections module that treats multiple dicts as a single view. Unlike | or {**d1, **d2}, ChainMap doesn't copy the data — it references the original dicts. Lookups check the first dict, then the second. This is efficient for large dicts or when the dicts change over time.
How do I merge more than two dictionaries?
For |, chain the operator: merged = d1 | d2 | d3 | d4. For {**}, unzip them: {**d1, **d2, **d3, **d4}. For update, chain the calls: for d in [d2, d3, d4]: d1.update(d). For many dicts (a list of unknown length), use reduce: merged = reduce(lambda a, b: a | b, dicts, {}).
Can I merge dictionaries without overwriting values?
Not with the built-in operators — they always overwrite on conflict. You need explicit logic: {**d1, **d2} keeps d2's value on conflict. If you want d1 to win: {**d2, **d1}. If you want to sum values on conflict: {k: d1.get(k, 0) + d2.get(k, 0) for k in set(d1) | set(d2)}. For deep merging, use mergedeep.
🔗 Continue learning
Next in this series:
sorted(d.items(), key=...) — and why you get a list, not a dict