How does collections.ChainMap resolve a lookup when several of its mappings hold the same key?
answer
- several mappings, one mapping-shaped view
- the order of the layers decides
- reads scan; writes do not
- no copy — references stay live
- new_child pushes, parents pops
basics
~20 sIt searches its mappings left to right and returns the first hit, so the front mapping shadows the ones behind it. Nothing is copied or merged: the underlying dicts stay live and later edits show through.
solid answer
~40 s`collections.ChainMap` holds an ordered list of mappings and searches them front to back on every lookup, returning the value from the first mapping that has the key; the rest are shadowed. It never merges or copies them — it keeps references, so mutating one of the underlying dicts is visible through the chain at once. Iteration and `len()` de-duplicate keys, so a key present in three layers appears once. Writes, `del` and `pop` touch **only the first mapping**, which is what makes the classic layered-configuration idiom work: put command-line overrides first, environment second, file defaults last, read through one mapping-shaped object, and let the front layer absorb any change.
code
python · 9 linesfrom collections import ChainMap
defaults = {"dpi": 150, "watermark": False}
overrides = {"dpi": 300}
cfg = ChainMap(overrides, defaults)
print(cfg["dpi"]) # 300 - the first mapping that has the key wins
print(cfg["watermark"]) # False - falls through to defaults
defaults["watermark"] = True # layers stay live; nothing was copied
print(cfg["watermark"], len(cfg), sorted(cfg))go deeper
Recall the two halves: reads scan the mappings front to back and take the first hit, writes only ever touch the first mapping. Being able to say "it does not copy anything" already puts you ahead.
Explain the mechanics: a list of mappings, per-lookup cost proportional to the number of layers, de-duplicated iteration and length, and KeyError when deleting a key that lives only in a deeper mapping.
Show when it beats merging — live layers, preserved provenance, O(1) scoping via new_child() — and where it costs you: not an exact dict, and lookups slow down as the chain grows.
Own the boundary decision: which layers exist, who may write to which, and where the configuration is flattened to a snapshot so a hot reload cannot change a value halfway through a unit of work.
## What a ChainMap actually is `collections.ChainMap` is a thin `collections.abc.MutableMapping` wrapper around a plain Python **list of mappings**, exposed as its `maps` attribute. The constructor takes the mappings as positional arguments and stores references to them; it copies nothing, allocates no combined dictionary, and does no work proportional to the data. `ChainMap(a, b, c)` is essentially the list `[a, b, c]` plus mapping behaviour bolted on top. ## Lookup: first match wins `cm[key]` walks the list front to back and returns the value from the **first** mapping whose `__getitem__` succeeds. If no mapping has the key, `__missing__` raises `KeyError`. Membership (`in`) and `get` follow the same front-to-back scan. So the front mapping *shadows* the ones behind it, in exactly the way a local name shadows a global one — the shadowed value is not lost, just not reachable through the chain while the front layer has that key. The cost model follows directly: a hit in the first layer is one dict lookup, and a miss that resolves in the last of N layers costs N lookups. With a handful of layers that is noise. With a chain thousands deep it is not, which is why the type is for configuration layering, not for use as a general accumulator. ## Iteration and length de-duplicate `len(cm)` reports the number of **distinct** keys, and iterating yields each key once even if four layers define it. The implementation builds that key set by walking the mappings from the last to the first, so the iteration order is dominated by the deepest layer's insertion order, with keys that only exist in nearer layers appended after. Do not write code that depends on that ordering; depend only on the de-duplication. ## Writes land in the front mapping only This is the half candidates forget. `cm[key] = value`, `del cm[key]`, `cm.pop(key)`, `cm.popitem()`, `cm.setdefault(...)` and `cm.update(...)` all operate on `maps[0]` and nothing else. Assignment therefore *adds a shadow* over any deeper value rather than editing where the value came from. Deletion is stricter still: deleting a key that exists only in a deeper mapping raises `KeyError` with the message "Key not found in the first mapping", because a `ChainMap` has no way to hide a deeper key — only to shadow it with a different value. That asymmetry — read through all layers, write to one — is the whole design. The front mapping is a scratch layer over read-only context. ## new_child and parents Two helpers manage the stack. `new_child()` returns a **new** `ChainMap` whose first mapping is a fresh empty dict (or the mapping you pass) followed by all the existing ones; the receiver is not modified. `parents` is a property returning a new `ChainMap` of everything **except** the first mapping — the view the front layer is shadowing. Together they model nested scopes: entering a scope is `new_child()`, and looking at the enclosing scope is `parents`. ## Why not just merge the dicts? Merging produces a flat snapshot. That is the right answer when you want a frozen, cheap-to-read result and you never need to know where a value came from. A `ChainMap` is the right answer when you want the opposite three properties: * **Live layers.** Reload the defaults file into the same dict and every reader sees the new values with no rebuild. * **Preserved provenance.** The layers are still separate objects, so you can ask which one supplied a value, or drop a layer. * **Cheap scoping.** `new_child()` is O(1) and discards in O(1); merging is O(total keys) each time. The trade you accept is per-lookup cost proportional to the number of layers, plus a mapping that is not a `dict` — code doing `isinstance(x, dict)` or handing the object to a C API that demands an exact dict will reject it. `dict(cm)` flattens it at that boundary. ## Where it shows up The canonical use is layered configuration: `ChainMap(cli_args, os.environ, file_defaults)` gives one mapping-shaped object with the precedence you want, and each source stays inspectable. The second use is simulating nested scopes in interpreters, template engines and rule evaluators, where `new_child()` pushes a frame and dropping the reference pops it. Both are cases where the layers are meaningful and you would have to reconstruct them after a merge.
- What does len() of a ChainMap report when the same key appears in three of its mappings?One. `len()` counts distinct keys, and iteration yields each key once no matter how many layers define it. Only the value from the frontmost layer holding it is reachable; the shadowed values still exist in their own dicts and reappear if the front layer's entry is removed.
- What happens if you delete a key from a ChainMap when only the last mapping contains it?`KeyError` — the message says the key was not found in the first mapping. Deletion only ever touches the first mapping, and a ChainMap cannot mask a deeper key; it can only shadow it with a different value. If you need the key gone, delete it from the mapping that actually owns it, or flatten with `dict(cm)` first.
- When would you flatten a ChainMap into a plain dict instead of passing the ChainMap around?At a boundary that needs a snapshot or an exact `dict`: serializing the effective configuration, handing it to code that does `isinstance(x, dict)`, or freezing values before a background worker so later layer edits cannot change them mid-run. `dict(cm)` costs one pass over the distinct keys and gives up liveness and provenance in exchange.
It works like a stack of transparencies on an overhead projector: you read the topmost mark for any spot, but you only ever draw on the top sheet, and the sheets underneath are unchanged and still separately removable.
saying these in an interview costs you the question
- Says ChainMap merges the dicts into a new one
- Thinks the last mapping wins the lookup
- Believes writes update wherever the value was found
- Expects len() to sum the layers' lengths
- Assumes edits to an underlying dict are not visible
- Calls it a copy, so it can be mutated safely