How does the `|` merge operator combine two Python dicts, and how does it differ from `dict.update`?
answer
- Two ways to combine mappings
- One of them returns None
- Right operand wins on duplicates
- Position from the left, value from the right
basics
~20 sa | b returns a new dict and leaves both operands alone; it exists since Python 3.9. a.update(b) mutates a and returns None. Both let the right-hand side win on duplicate keys, and both copy one level deep.
solid answer
~40 sThe `|` operator on dicts came in Python 3.9 with PEP 584: `a | b` returns a **new** dict containing both, with `b`'s value winning any key collision, and neither operand is modified. `a |= b` is the in-place form and, like `update`, also accepts an iterable of key/value pairs on the right, while `a | [("k", 1)]` raises `TypeError`. `a.update(b)` mutates `a` and returns `None`, so `merged = a.update(b)` is a bug that silently binds `None`. `{**a, **b}` has done the same job since 3.5 and generalises to more operands. All of them are shallow: nested dicts are shared with the source, so mutating one through the merged dict changes the other. And a merged key keeps its *position* from the left operand while taking its *value* from the right.
code
python · 6 linesbase = {"currency": "EUR", "retries": 2}
override = {"retries": 5, "timeout": 30}
print(base | override)
print(override | base)
print(base.update(override))
print(base)go deeper
Know at least two ways to combine dicts and which one changes the original: update mutates and returns nothing, | and {**a, **b} hand you a new dict. Remember that the right-hand side wins on a clash.
Explain the differences precisely - the return value of update, the 3.9 arrival of |, the fact that |= accepts pairs while | demands a mapping, and that every form is shallow so nested values stay shared.
Bring up the traps that bite in production: shallow sharing of nested configuration, key order coming from the left operand when downstream output depends on it, and choosing |= inside a loop rather than allocating a fresh dict per iteration.
Own the layering policy. Decide how configuration precedence is expressed and enforced across services - which layer sits on which side, whether nested values replace or merge, and where that is implemented once rather than re-derived in each component.
Python has three spellings for combining two dicts, and they differ in what they return, what they mutate, and what they accept. **`a.update(b)`** has existed forever. It copies `b`'s entries into `a`, overwriting on collision, and returns `None` — the standard convention for in-place mutators, alongside `list.sort` and `list.append`. Writing `merged = a.update(b)` therefore binds `None`, which is one of the most common beginner bugs on this leaf. `update` is also the most permissive of the three about its right-hand side: it accepts any mapping, any iterable of key/value pairs, and keyword arguments, so `d.update([("k", 1)], region="eu")` is valid. **`{**a, **b}`** has worked since Python 3.5 (PEP 448). It builds a brand-new dict by unpacking both operands, right-hand wins, and generalizes to any number of operands and to inline extra keys — `{**defaults, **overrides, "trace_id": tid}`. **`a | b`** arrived in Python 3.9 (PEP 584) and is the most readable of the three for the two-dict case. It returns a **new** dict and leaves both operands untouched; on 3.8 and earlier the same expression raises `TypeError`. Its in-place sibling `a |= b` mutates `a` and, like `update` and unlike `|`, accepts an iterable of pairs on the right — `a |= [("k", 1)]` works while `a | [("k", 1)]` raises `TypeError`. One further asymmetry: for a `dict` subclass on the left, `|` returns a plain `dict`, dropping the subclass, while `|=` mutates in place and so preserves it. **Which value wins.** In all three forms, the *right-hand* operand wins on a duplicate key. `{"retries": 2} | {"retries": 5}` is `{"retries": 5}`. This is the "defaults on the left, overrides on the right" convention, and reading it backwards is the classic mistake. **Which position wins — the subtler half.** A merged key takes its **value** from the right operand but keeps its **position** from the left, because the merge is implemented as "copy the left, then update with the right", and updating an existing key never moves it. So: ```python base = {"currency": "EUR", "retries": 2} override = {"retries": 5, "timeout": 30} base | override # {'currency': 'EUR', 'retries': 5, 'timeout': 30} override | base # {'retries': 2, 'timeout': 30, 'currency': 'EUR'} ``` Note the second line: the merged dict is ordered like `override`, but `retries` carries `base`'s value of 2. Value precedence and key order come from opposite operands, and any code that treats a merged dict as ordered — writing a payroll CSV whose column order is taken from `merged.keys()`, for instance — is depending on the *left* operand's order, not on the one the reader probably assumed. That is a bug that produces a perfectly valid file with silently shuffled columns. If output order is part of your contract, state it explicitly with a key list rather than inferring it from a merge. **All three are shallow.** A merge copies references, one level deep. If a value is itself a dict or a list, the merged dict and the source share that object: ```python defaults = {"limits": {"rpm": 1200}} merged = defaults | {"region": "eu"} merged["limits"]["rpm"] = 60 # defaults is now {'limits': {'rpm': 60}} ``` Nothing in the merge protects you here, and neither does `dict.copy()`, which is also shallow. When independence matters, `copy.deepcopy` the nested structure, or write a small recursive merge — Python has no built-in deep merge, and interviewers like asking you to write one, because it forces you to say what should happen when the two sides disagree about a key's *type*. **Choosing.** For two dicts and a new result, `|` reads best and states the intent. For accumulating into an existing dict inside a loop, `update` or `|=` avoids allocating a dict per iteration. For merging more than two, or for adding literal keys at the same time, `{**a, **b, "k": v}` is the most compact. And when the values themselves are containers you intend to combine rather than replace, none of the three is what you want — you need an explicit merge function, because every built-in form replaces the whole value. **The legacy spelling.** Before PEP 448 you saw `dict(a, **b)`, which builds a new dict from `a` and then applies `b` as keyword arguments. It still works, but only when every key in `b` is a string - `dict(a, **{1: 2})` raises `TypeError: keywords must be strings` - so it is strictly weaker than the alternatives and worth recognising in old code rather than writing. `dict(a)` on its own is simply a shallow copy, the same as `a.copy()`. **Accumulating.** Inside a loop, prefer mutation over rebuilding. `merged |= chunk` (or `merged.update(chunk)`) writes into one dict, while `merged = merged | chunk` allocates and copies a fresh dict on every pass, turning a linear accumulation into a quadratic one. The functional-looking spelling is the expensive one, and this is the shape of the mistake that shows up as a job getting slower as its input grows.
- Why does `settings = settings.update(overrides)` leave you holding None?`update` mutates in place and returns `None`, the same convention as `list.sort` and `list.append`, so the assignment throws away the dict and binds `None`. Call it as a statement - `settings.update(overrides)` - or use an expression form that returns a dict: `settings = settings | overrides`, or `settings |= overrides` to mutate in place with the operator.
- What happens to nested dictionaries when you merge with `|`?Nothing protective: the merge copies references one level deep, so a nested dict is the same object in both the source and the result, and mutating it through either name is visible through the other. `dict.copy()` is shallow for the same reason. Use `copy.deepcopy` or a purpose-written recursive merge when the two sides must be independent.
- What type does `|` return when the left operand is a dict subclass?A plain `dict` - the subclass is dropped, because `|` builds a new object rather than delegating construction to the subclass. `|=` mutates the existing object in place, so it preserves the subclass. That asymmetry catches people who use a dict subclass to carry extra behaviour and lose it on the first merge.
saying these in an interview costs you the question
- Says `d.update(other)` returns the merged dict
- Thinks `a | b` mutates the left operand
- Believes the left-hand dict wins on duplicate keys
- Assumes `|` deep-copies nested dictionaries
- Claims `{**a, **b}` and `a | b` disagree on which value wins
- Expects a merged key to move to the right operand's position