ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 12

  1. Blog

ChainMap vs Merged Dict Lookup: Localhost Lab

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.

Aditya Challa·1 October 2026·3 min read

Summary
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — warm lookups (p50)
  5. Overlay rebuild (one sweep)
  6. Config stack pattern
  7. vs tomllib/json loads
  8. Reading it
  9. Write semantics
  10. Memory shape
  11. When sequential `in` appears
  12. Pitfalls
  13. Reproduce
  14. Limits
  15. Takeaway

Intro — what this post promises

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​

  • stat vs path stat localhost labmmap write vs write 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.


Arms

ArmPatternChainMap(cli, env, defaults).getlayered getChainMap[...]layered getitemmerged dict .getflatten oncesequential in cli→env→defaultshand walkrebuild merge + one sweepcold overlaynew ChainMap + one sweepcheap view


Lab topology

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.


Lead table — warm lookups (p50)

ArmMops/smerged dict.get21.06sequential layer in18.86ChainMap getitem3.65ChainMap.get1.71


Overlay rebuild (one sweep)

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.


Config stack pattern

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.


vs tomllib/json loads

Labs 112/104 measure parse cost. This lab measures post-parse layered lookup. Parse into dicts, then decide ChainMap vs merge for serving reads.


Reading it

  • Stable flags / hot path → merge once, look up on one dict.
  • Live overlays (feature flags, request-scoped defaults) → ChainMap without rewriting the base.
  • ChainMap.get trailed __getitem__ here — prefer cm[k] when keys are known present.
  • Semantics matched merge on every key in the fixture.

Write semantics

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.


Memory shape

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.


When sequential in appears

Hand-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.


Pitfalls

  • Mutating a mapping that sits inside a ChainMap (shared views).
  • Expecting ChainMap writes to deep-merge nested dicts (they don’t).
  • Rebuilding a merged dict on every request when only one layer changed.
  • Using ChainMap for speed — it is for structure, not peak Mops/s.

Reproduce

python3 lab-evidence/118-chainmap-vs-dict-merge/results/run_lab.py

Evidence: summary.json, summary.txt.


Limits

One Linux box. Flat string values. Not nested deep-merge. Not thread-safe mutation patterns.


Takeaway

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.

pythonchainmapdictionaryperformancebenchmarkingconfiglookuplatency

Lab evidence

What I found running this

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.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Related links

  • Plate 68

    gc.collect Cost Empty vs Cycles: Localhost Lab

    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.

    Observability & SRE · 1 Oct 2026

  • Plate 34

    ast.literal_eval vs json.loads: Localhost Lab

    1 Oct 2026

  • Plate 76

    html.parser vs regex Tag Strip: Localhost Lab

    1 Oct 2026

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — warm lookups (p50)
  5. Overlay rebuild (one sweep)
  6. Config stack pattern
  7. vs tomllib/json loads
  8. Reading it
  9. Write semantics
  10. Memory shape
  11. When sequential `in` appears
  12. Pitfalls
  13. Reproduce
  14. Limits
  15. Takeaway
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove