How to Merge Dictionaries in Python: The Complete Guide

Merging two dictionaries into one is among the most commonly encountered operations in Python. It comes up when combining configuration files, aggregating API responses, flattening nested data, or simply updating default settings with user-provided overrides. And yet, for years, Python lacked a single clean, obvious way to do it. This article walks through every technique, explains why it exists, and shows you exactly when to reach for each one.

Method 1: The update() Method

The dict.update() method has been part of Python since the beginning. It merges the key-value pairs from one dictionary into another, overwriting any keys that already exist. It is the simplest approach, but it carries an important constraint: it mutates the dictionary you call it on.

defaults = {"theme": "dark", "lang": "en", "font_size": 14}
user_prefs = {"theme": "light", "font_size": 18}

defaults.update(user_prefs)

print(defaults)
{'theme': 'light', 'lang': 'en', 'font_size': 18}

The user preferences overwrote the defaults for "theme" and "font_size", while "lang" remained untouched. This is exactly the behavior you want when applying overrides -- and the last-seen value wins pattern will reappear in every merging method we cover.

The drawback is that defaults has been modified in place. If you need the original dictionary preserved, you have to copy it first:

merged = defaults.copy()
merged.update(user_prefs)

# defaults is unchanged; merged holds the combined result

This two-step pattern -- copy then update -- was the standard idiom for years. It works in every version of Python, it is explicit, and it is immediately readable. The only downside is that it cannot be expressed as a single expression, which matters when you need a merged dictionary inline, such as inside a function call or a comprehension.

Watch Out: Shallow Copy Only

Both dict.copy() and the merge techniques that follow produce shallow copies. If your dictionary values are mutable objects like lists or nested dictionaries, the copy will share references to those objects. Mutating a nested list in the merged dictionary will also mutate it in the original. For deeply nested structures, use copy.deepcopy().

Method 2: The dict() Constructor Trick

Before the unpacking generalizations arrived, one popular workaround was to pass one dictionary as positional argument and the other as keyword arguments to the dict() constructor:

defaults = {"theme": "dark", "lang": "en"}
user_prefs = {"theme": "light", "font_size": 18}

merged = dict(defaults, **user_prefs)

print(merged)
{'theme': 'light', 'lang': 'en', 'font_size': 18}

This is a single expression, which solves the inline problem. But it has a significant limitation: the second dictionary's keys must all be strings, because the ** operator unpacks them as keyword arguments, and keyword arguments in Python must be valid identifiers. If the second dictionary contains a non-string key such as an integer, Python raises a TypeError:

d1 = {"a": 1}
d2 = {42: "answer"}

merged = dict(d1, **d2)
# TypeError: keywords must be strings

Guido van Rossum himself commented on this pattern. Writing on the python-dev mailing list, he described dict(x, **y) as a misuse of the keyword argument mechanism. He characterized it not as a clever feature to embrace, but rather as something he considered an abuse of the ** mechanism. Because of this limitation and the creator's explicit disapproval, this technique is best treated as a historical curiosity rather than a recommended practice.

Method 3: Dictionary Unpacking with {**d1, **d2}

Python 3.5, released in September 2015, introduced a major improvement to dictionary merging through PEP 448. This proposal, authored by Joshua Landau, generalized the use of the * and ** unpacking operators to allow them in more positions and more contexts than before -- including inside dictionary display literals.

PEP

Additional Unpacking Generalizations — Author: Joshua Landau. Status: Final. Python version: 3.5. Created June 29, 2013. This PEP proposed extending the * and ** unpacking operators to allow unpacking in more positions, an arbitrary number of times, and in additional contexts including function calls and display literals.

The key insight of PEP 448 was that if you can already use **d to unpack a dictionary into function keyword arguments, you should be able to use the same syntax to unpack dictionaries into a dictionary literal. That gave us the {**d1, **d2} pattern:

defaults = {"theme": "dark", "lang": "en", "font_size": 14}
user_prefs = {"theme": "light", "font_size": 18}

merged = {**defaults, **user_prefs}

print(merged)
{'theme': 'light', 'lang': 'en', 'font_size': 18}

This approach creates a brand new dictionary without modifying either original, works as a single expression, and -- unlike the dict() constructor trick -- handles non-string keys without any issues:

d1 = {1: "one", 2: "two"}
d2 = {2: "TWO", 3: "three"}

merged = {**d1, **d2}
print(merged)
{1: 'one', 2: 'TWO', 3: 'three'}

PEP 448 also explicitly codified the conflict resolution rule: in dictionary displays, later values always override earlier ones. The order of the unpacked dictionaries determines which value wins. In the example above, d2's value for key 2 overwrites d1's because **d2 appears after **d1 in the literal.

You can also mix unpacking with literal key-value pairs and chain more than two dictionaries:

base = {"host": "localhost", "port": 8080}
overrides = {"port": 9000}

config = {**base, **overrides, "debug": True}

print(config)
{'host': 'localhost', 'port': 9000, 'debug': True}

Despite its power, this syntax drew criticism for being unintuitive. When PEP 584 was being drafted four years later, Guido van Rossum wrote on the python-ideas mailing list in February 2019 that he doubted many typical Python users would think of {**d1, **d2} as the way to combine two dictionaries, and admitted that even he himself had forgotten about the technique. That candid admission from Python's creator became a central argument for adding a dedicated merge operator.

Limitation: Always Returns a Plain dict

The {**d1, **d2} syntax ignores the type of the source mappings and always returns a plain dict. If d1 is a defaultdict or OrderedDict, the merged result will still be a regular dict. Attempting to preserve the subclass type with type(d1)({**d1, **d2}) fails for subclasses like defaultdict that have incompatible __init__ methods. This was another motivating factor behind PEP 584.

Method 4: The Merge Operator | (Python 3.9+)

Python 3.9, released on October 5, 2020, introduced what many consider the definitive answer to dictionary merging: the | (pipe) operator and its in-place counterpart |=. This was the result of PEP 584, authored by Steven D'Aprano and Brandt Bucher, with Guido van Rossum himself serving as the BDFL-Delegate who approved it.

PEP

Add Union Operators To dict — Authors: Steven D'Aprano, Brandt Bucher. BDFL-Delegate: Guido van Rossum. Status: Final. Python version: 3.9. Created March 1, 2019. Accepted February 17, 2020. This PEP added the merge (|) and update (|=) operators to the built-in dict class.

When Guido van Rossum formally accepted PEP 584 on February 17, 2020, he wrote on the python-dev mailing list that the final design decisions had been settled and that the implementation would not call the copy or update methods internally. It had been a long road -- the idea of dictionary "addition" had been independently proposed by community members for years before a PEP was finally drafted.

The syntax is clean and immediately readable:

defaults = {"theme": "dark", "lang": "en", "font_size": 14}
user_prefs = {"theme": "light", "font_size": 18}

# Creates a new dict -- neither original is modified
merged = defaults | user_prefs

print(merged)
{'theme': 'light', 'lang': 'en', 'font_size': 18}

The | operator borrows its symbol from set union, which was a deliberate design choice. During the PEP's development, there was extensive debate about whether to use + (addition) or | (union). Stefan Behnel, a core contributor, argued on python-ideas that lists and tuples already use + for concatenation and sets use | for union, and since dictionaries are conceptually closer to sets -- unordered collections of unique keys -- the pipe operator was the more logical analogy. Van Rossum initially favored + but later stated in his December 2019 review of PEP 584 that he warmed up to | and recommended the Steering Council move forward with it, targeting Python 3.9.

The In-Place Update Operator: |=

Just as += extends a list in place, |= updates a dictionary in place:

config = {"host": "localhost", "port": 8080, "debug": False}
updates = {"port": 9000, "debug": True}

config |= updates

print(config)
{'host': 'localhost', 'port': 9000, 'debug': True}

A subtle but important asymmetry exists between | and |=. The binary | operator requires both operands to be dict instances (or subclasses). But the augmented assignment |= accepts anything implementing the Mapping protocol -- or even an iterable of key-value pairs. This mirrors how list += accepts any iterable, not just lists:

inventory = {"apples": 3, "bananas": 5}

# Update from a list of tuples
inventory |= [("cherries", 12), ("apples", 10)]

print(inventory)
{'apples': 10, 'bananas': 5, 'cherries': 12}

But the binary merge operator rejects non-dict operands:

inventory = {"apples": 3}

inventory | [("cherries", 12)]
# TypeError: unsupported operand type(s) for |: 'dict' and 'list'

The PEP 584 authors considered this asymmetry carefully. In the PEP's specification section, they wrote that allowing the in-place operator to accept a wider range of types, as list does, was the more useful design, while restricting the binary operator's operands would help avoid silent errors from implicit type casting.

Chaining Multiple Dictionaries

The | operator chains naturally:

base = {"a": 1, "b": 2}
layer1 = {"b": 20, "c": 30}
layer2 = {"c": 300, "d": 400}

final = base | layer1 | layer2

print(final)
{'a': 1, 'b': 20, 'c': 300, 'd': 400}

Be aware, though, that chaining creates intermediate dictionaries. The expression d | e | f | g builds and discards three temporary dictionaries. The PEP 584 authors acknowledged this, but noted it is no different from the quadratic behavior of repeated list concatenation with +, and that in practice it is rare for people to merge large numbers of dictionaries in a chain. Their survey of the standard library found no examples of merging more than two dictionaries at once.

Method 5: collections.ChainMap

The ChainMap class from the collections module deserves mention as an alternative that does not merge at all -- it creates a unified view of multiple dictionaries without copying any data:

from collections import ChainMap

defaults = {"theme": "dark", "lang": "en"}
user_prefs = {"theme": "light"}

# user_prefs takes priority because it is listed first
combined = ChainMap(user_prefs, defaults)

print(combined["theme"])
print(combined["lang"])
print(dict(combined))
light

            en

            {'theme': 'light', 'lang': 'en'}

There are two critical differences from a true merge. First, ChainMap uses a first-seen-wins resolution order -- the first mapping in the chain takes priority, not the last. This is the opposite of every other method we have covered. Second, ChainMap wraps the underlying dictionaries by reference. Writing to the ChainMap modifies the first underlying dictionary:

combined["lang"] = "fr"
print(user_prefs)
{'theme': 'light', 'lang': 'fr'}

PEP 584 explicitly noted that ChainMap is unfortunately poorly-known among Python users and does not qualify as an obvious solution. It works best for layered configuration systems where you genuinely want a view that reflects live changes to the underlying dictionaries, not for producing a single standalone merged result.

The Complete Comparison

Method Python Version Returns New Dict? Single Expression? Non-String Keys?
d1.update(d2) All No (in-place) No Yes
dict(d1, **d2) All Yes Yes No
{**d1, **d2} 3.5+ Yes Yes Yes
d1 | d2 3.9+ Yes Yes Yes
d1 |= d2 3.9+ No (in-place) Yes Yes
ChainMap(d2, d1) 3.3+ View (no copy) Yes Yes

Conflict Resolution: The Last-Seen-Wins Rule

Every merge technique in Python follows the same conflict resolution rule: when a key appears in both dictionaries, the value from the right-hand (or later) operand wins. This is consistent across update(), {**d1, **d2}, and d1 | d2. PEP 584 codified this explicitly in its specification: key conflicts are resolved by keeping the rightmost value. The PEP noted that this matches the existing behavior of dictionary literals themselves (where {"a": 1, "a": 2} keeps the value 2), dictionary comprehensions, and dict.update().

The order of keys in the resulting dictionary also follows a consistent rule: keys appear in the order they are first encountered, scanning left to right. Duplicate keys retain their original position but get their value overwritten. This means dictionary union is not commutative -- in general, d1 | d2 produces a different key ordering than d2 | d1:

d = {"spam": 1, "eggs": 2, "cheese": 3}
e = {"cheese": "cheddar", "aardvark": "Ethel"}

print(d | e)
print(e | d)
{'spam': 1, 'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel'}

            {'cheese': 3, 'aardvark': 'Ethel', 'spam': 1, 'eggs': 2}

The values differ (cheese is "cheddar" vs 3) and the key order differs too. This was a point of debate during PEP 584's review, but the authors took the position that non-commutativity is the simple, obvious, and expected behavior.

Deep Merging: What Python Doesn't Do For You

None of the built-in methods perform a deep merge. If a key exists in both dictionaries and both values are themselves dictionaries, a shallow merge simply overwrites the entire nested dictionary from the first with the one from the second:

a = {"db": {"host": "localhost", "port": 5432}}
b = {"db": {"port": 3306}}

print(a | b)
{'db': {'port': 3306}}

The "host" key is gone entirely. If you need recursive merging, you have to write it yourself:

def deep_merge(base: dict, override: dict) -> dict:
    """Recursively merge override into base. Returns a new dict."""
    result = base.copy()
    for key, value in override.items():
        if (
            key in result
            and isinstance(result[key], dict)
            and isinstance(value, dict)
        ):
            result[key] = deep_merge(result[key], value)
        else:
            result[key] = value
    return result

a = {"db": {"host": "localhost", "port": 5432}}
b = {"db": {"port": 3306}}

print(deep_merge(a, b))
{'db': {'host': 'localhost', 'port': 3306}}

Now "host" is preserved while "port" is correctly overridden. This pattern comes up constantly in configuration management and is worth keeping in your utility module.

Looking Ahead: PEP 798 and Dictionary Comprehension Unpacking

The story of dictionary merging in Python is still being written. PEP 798, titled "Unpacking in Comprehensions," was accepted by the Python Steering Council in November 2025 and is targeted for Python 3.15, expected in October 2026. This PEP extends PEP 448's unpacking generalizations into comprehensions and generator expressions.

PEP

Unpacking in Comprehensions — Status: Accepted. Python version: 3.15. This PEP extends list, set, and dictionary comprehensions to allow unpacking notation (* and **) at the start of the expression, providing a concise way of combining an arbitrary number of iterables or dictionaries.

With PEP 798, you will be able to merge an arbitrary number of dictionaries using a dictionary comprehension:

# Merge an arbitrary list of dictionaries
configs = [
    {"host": "localhost"},
    {"port": 8080},
    {"debug": True, "port": 9000},
]

merged = {**d for d in configs}
# Expected: {'host': 'localhost', 'port': 9000, 'debug': True}

The PEP's rationale section includes a compelling anecdote: the proposal was motivated partly by a written exam in a Python programming class where several students used the proposed notation in their solutions, assuming it already existed. This suggests the syntax represents a natural, logical extension of what Python programmers already expect to work. The current alternative -- the double-loop version {k: v for d in configs for k, v in d.items()} -- is not only more verbose, but students often reverse the order of the for clauses when writing it from memory.

Which Method Should You Use?

Here is the practical decision framework.

If you are writing Python 3.9 or later (which you should be -- Python 3.8 reached end of life in October 2024), use the | operator for creating a new merged dictionary, and |= for updating in place. They are the most readable, most discoverable, and most Pythonic options available today. This is what PEP 584 was designed to give us.

If you are maintaining a codebase that still supports Python 3.5 through 3.8, use {**d1, **d2}. It works, handles all key types, and is a single expression. Just accept that it is less immediately obvious to readers unfamiliar with the unpacking syntax.

If you need to mutate a dictionary in place and want maximum compatibility, use d1.update(d2). There is nothing wrong with the oldest method -- it is explicit, well-understood, and widely supported.

If you need deep merging of nested dictionaries, none of the built-in methods will help you. Write a recursive helper function or use a library that provides one.

And if you need a live layered view without copying data, reach for ChainMap -- but know that it resolves conflicts differently and writes go to the first underlying dictionary.

Key Takeaway

Python's journey from having no clean way to merge dictionaries to having a dedicated | operator took two PEPs (448 and 584), seven years of discussion, and the personal involvement of Guido van Rossum as BDFL-Delegate. The result is a language where the right tool exists for every merging scenario -- as long as you understand what each one does and when to use it.

cd ..