Hands-on collections.ChainMap vs merged dict config lookup: real ops/s on layered keys, measured on Linux localhost today in this hands-on lab for SREs.
3 min read
Layered config lookup: collections.ChainMap vs a single merged dict vs manual sequential in across layers. This lab reports ops/s on Linux localhost.
Related links:
tomllib vs json localhost lab
path glob vs fnmatch localhost lab
dataclass replace vs manual localhost lab
zoneinfo vs utc offset localhost lab
heapq merge vs sorted localhost lab
islice vs list slice localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. 500 keys; layers cli ⊃ env ⊃ defaults.
Verdict up front: merged dict.get ~21.06 Mops/s; sequential layer in ~18.86; ChainMap.__getitem__ ~3.65; ChainMap.get ~1.71. ChainMap wins when overlays change often; merge wins raw lookup speed.
ArmPatternChainMap(cli, env, defaults).getlayered getChainMap[...]layered getitemmerged dict .getflatten oncesequential in cli→env→defaultshand walkrebuild merge + one sweepcold overlaynew ChainMap + one sweepcheap view
500 keys · cli/env/defaults overlays · 200 full sweeps · 7 rounds · p50
metric: ops/s = (sweeps × keys) / p50_s
Script: lab-evidence/118-chainmap-vs-dict-merge/results/run_lab.py.
ArmMops/smerged dict.get21.06sequential layer in18.86ChainMap getitem3.65ChainMap.get1.71
ArmMops/srebuild merge + sweep18.69new ChainMap + sweep3.96
Creating a ChainMap view is cheap versus copying all keys into a new dict when layers mutate frequently — even though steady-state lookups favor the merged map.
A common SRE shape is ChainMap(cli_args, os_environ_subset, file_defaults). Keep file defaults immutable and push ephemeral overrides on the left. That avoids defaults.copy(); defaults.update(env) on every boot when only argv changes.
Labs 112/104 measure parse cost. This lab measures post-parse layered lookup. Parse into dicts, then decide ChainMap vs merge for serving reads.
ChainMap.get trailed __getitem__ here — prefer cm[k] when keys are known present.cm[k] = v writes into the first mapping (leftmost). That is usually what you want for overrides, and dangerous if the leftmost map is a shared process-global. Prefer a dedicated per-request dict on the left so defaults stay pristine.
ChainMap stores references, not copies. A merged dict duplicates keys/values into one table — faster gets, higher RAM, and a snapshot that drifts if someone mutates a source layer afterward. Pick snapshot-vs-live deliberately.
in appearsHand-rolled if k in cli elif k in env matched merge speed closely here (~18.9 Mops/s). It still duplicates precedence logic at every call site. ChainMap encodes precedence once; merge encodes it at build time — both beat scattering if ladders across services.
python3 lab-evidence/118-chainmap-vs-dict-merge/results/run_lab.py
Evidence: summary.json, summary.txt.
One Linux box. Flat string values. Not nested deep-merge. Not thread-safe mutation patterns.
Warm lookups: merged dict ~21.06 Mops/s beat ChainMap getitem ~3.65 Mops/s. Use ChainMap for layered overlays; flatten when lookup rate dominates.
Lab evidence
Ran the supplied ChainMap benchmark on the Linux localhost fixture and checked the rendered body against the source. The warm lookup ordering surprised me: merged dict.get was far ahead, while ChainMap stayed useful for live overlays. I also verified the related links remained clickable and the comparison table rendered as a real table.
Hands-on gc.collect cost empty vs cyclic garbage lab: real collect latency plus reclaim counts, measured on Linux localhost today in this lab for SREs.
Hands-on1 Oct 20264 min read
Summary1 Oct 20263 min read
Hands-on1 Oct 20264 min read