Python Dictionary Union Operator: Merging Dictionaries
python dictionary union operator: Learn how to merge dictionaries in Python using the union operator (|), including syntax, duplicate handling, precedence, and when to...
python dictionary union operator requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In Python 3.9, the dictionary union operator | gives you a concise way to merge two dictionaries into a new dictionary. This operator, introduced in PEP 584, is the direct syntax for combining mappings without mutating the original objects. If you have been using {**a, **b} or dict(a, **b), the union operator offers a more readable alternative that behaves consistently for both dict and other mapping types that implement the same protocol.
The Union Operator for Dictionaries
The syntax is straightforward:
merged = dict_a | dict_b
The result is a new dictionary containing all key-value pairs from dict_a and dict_b. When a key appears in both, the value from dict_b wins. The original dictionaries are left unchanged. This is a pure operation, which makes it easy to reason about in functional-style code.
config = {"host": "localhost", "port": 8080} override = {"port": 9090, "debug": True} merged = config | override print(merged) # {'host': 'localhost', 'port': 9090, 'debug': True}
The union operator also supports in-place merging with |=. This mutates the left-hand dictionary instead of creating a new one:
config = {"host": "localhost", "port": 8080} config |= {"port": 9090} print(config) # {'host': 'localhost', 'port': 9090}
The in-place form is useful when you want to update a dictionary without assigning the result to a new variable.
How the Union Operator Handles Duplicate Keys
When both dictionaries contain the same key, the right-hand operand takes precedence. This is the same rule used by {**a, **b} and by dict.update(). The key is not duplicated; the value is simply replaced.
left = {"a": 1, "b": 2} right = {"b": 3, "c": 4} result = left | right print(result) # {'a': 1, 'b': 3, 'c': 4}
The behavior is deterministic and does not depend on insertion order. The resulting dictionary preserves the insertion order of the left operand, then appends keys from the right operand that are not already present. This ordering is guaranteed for Python 3.7+ where dicts preserve insertion order.
Operator Precedence and Evaluation Order
The union operator has the same precedence as the bitwise OR operator for integers. In practice, this means it binds less tightly than method calls and attribute access, but more tightly than comparisons and boolean operators. If you are combining multiple dictionaries, the operation is left-associative:
result = a | b | c
This is equivalent to (a | b) | c. Each step produces a new dictionary, so the intermediate result is used as the left operand for the next union. There is no short-circuiting because all operands are evaluated before the operation.
Be careful when mixing the union operator with other operators in expressions. For example, if you need to merge dictionaries and then check membership, wrap the union in parentheses:
if ("key" in (base | override)): ...
Without parentheses, the expression may be parsed differently depending on the surrounding operators.
Comparing the Union Operator with Other Merge Methods
Before Python 3.9, developers used several patterns to merge dictionaries. Each has its own tradeoffs. The table below summarizes the main differences:
| Method | Mutates original | Handles non-string keys | Readability | Requires Python 3.9+ |
|---|---|---|---|---|
{**a, **b} | No | Yes | Good | No |
dict(a, **b) | No | Only for string keys | Moderate | No |
a.update(b) | Yes | Yes | Moderate | No |
a | b | No | Yes | Excellent | Yes |
The dict(a, **b) form fails if any key in b is not a string, because ** unpacking requires string keys. The {**a, **b} form works for any hashable keys, but the syntax is less obvious to newcomers. The update() method mutates the original dictionary, which may be undesirable when you need to keep the original intact.
The union operator combines the non-mutating behavior of {**a, **b} with a clearer, operator-based syntax. It also works with any mapping that implements the __or__ method, so custom mapping types can participate in the same protocol.
Performance and Memory Behavior
The union operator creates a new dictionary and copies all key-value pairs from both operands. This means the time complexity is O(n + m), where n and m are the sizes of the two dictionaries. Memory usage is also proportional to the total number of keys, since a new hash table is allocated.
For small dictionaries, the overhead is negligible. For very large dictionaries, you may want to consider whether you need a copy at all. If you can mutate one of the dictionaries, using update() avoids allocating a new dictionary and may be more memory-efficient. However, the union operator is still the clearest way to express a non-destructive merge.
There is no special optimization that avoids copying when one dictionary is empty. Even {} | large_dict creates a shallow copy of large_dict. If you only need a copy, dict(large_dict) is a more direct way to express that intent.
Compatibility and Version Requirements
The dictionary union operator was introduced in Python 3.9. If you are working on a codebase that must support Python 3.8 or earlier, you cannot use this syntax without a syntax error. The in-place |= operator is also part of the same PEP.
For projects that target multiple Python versions, you have two options. One is to use the {**a, **b} idiom, which works in all Python 3.x versions. Another is to use a helper function that picks the appropriate method at runtime, but this adds indirection and is rarely worth the complexity.
When using type checkers or linters, make sure your tooling is configured for Python 3.9 or later. Some static analysis tools may not recognize the operator if the target version is set to an older Python.
Practical Usage Patterns and Edge Cases
The union operator works with any mapping that implements the __or__ method. The standard dict class does, and so do collections.OrderedDict and collections.defaultdict. However, the result of ordered_dict | other is a regular dict, not an OrderedDict. This is a subtle behavior change to keep in mind if you rely on the specific type of the left operand.
from collections import OrderedDict, defaultdict od = OrderedDict([("a", 1)]) dd = defaultdict(int, {"b": 2}) print(type(od | {"c": 3})) # <class 'dict'> print(type(dd | {"c": 3})) # <class 'dict'>
If you need the result to remain a defaultdict or OrderedDict, you should explicitly convert it after the merge or use a different merge strategy.
Another edge case is merging dictionaries with the same key but values that are mutable objects. The union operator performs a shallow merge. The value object itself is not copied; both the original and the merged dictionary reference the same object. If you modify that object through one dictionary, the change is visible in the other.
shared = {"items": []} merged = {"a": 1} | shared merged["items"].append("x") print(shared["items"]) # ['x']
This is the same behavior as {**a, **b} and is generally what developers expect. If you need a deep merge, you must implement it separately.
The union operator is also useful in configuration loading, where defaults are combined with user-supplied overrides. Because it does not mutate the original defaults, you can reuse the same defaults dictionary across multiple calls without side effects.
DEFAULTS = {"host": "localhost", "port": 8080} def get_config(overrides=None): return DEFAULTS | (overrides or {})
This pattern is concise and makes the precedence rule explicit: overrides win.