Problem Solved

Python · merge dictionaries

Home › Courses › Python › Problems › Merge dictionaries
🐍 Concept · 3 min read

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.

merge.py
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.

or-vs-ior.py
# | 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.

chainmap.py
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.

1. Using | on Python 3.8

TypeError — the operator doesn't exist. Fix: {'**d1, **d2'}.

2. Expecting .update() to return a dict

It returns None. Fix: use | if you need a return value.

3. Confusing | and |=

| creates a new dict; |= mutates. Fix: use | for new dicts, |= for in-place.

4. Expecting deep merge

Nested dicts are replaced, not merged. Fix: use mergedeep or explicit recursion.

5. Wrong precedence assumption

The right dict wins on conflict. Fix: reverse the order if you want the left to win.

6. Using ChainMap when a dict is expected

ChainMap is a view, not a dict. Fix: convert with dict(chainmap) if needed.

7. Modifying a dict while merging

Confusing side effects. Fix: make a copy first, or use |.

8. Merging with +

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

🗺️ Python learning path
The full roadmap from install to first project — every concept in order

Next in this series:

🔀 How to sort a dictionary by value
sorted(d.items(), key=...) — and why you get a list, not a dict
← All Python problems
Problem solved · Python