Python ChainMap: Layered Lookups and Configuration Hierarchies Made Simple
ChainMap holds multiple dictionaries as a sequence and treats them as a single view. When you look up a key, it searches each dict in order and returns the first match. When you write a key, it goes into the first dict only, leaving the others untouched.
This is the right tool for any layered configuration pattern: CLI arguments override environment variables override a config file override hard-coded defaults — all without merging or copying dicts.
Basic Usage
from collections import ChainMap
defaults = {"color": "blue", "size": "medium", "debug": False}user_settings = {"color": "red"}session_overrides = {"debug": True}
# Priority: session_overrides → user_settings → defaultsconfig = ChainMap(session_overrides, user_settings, defaults)
print(config["color"]) # "red" — from user_settingsprint(config["size"]) # "medium" — from defaultsprint(config["debug"]) # True — from session_overridesThe dicts are searched left to right. The first dict gets the highest priority.
Layered Configuration Pattern
The most common real-world use: command-line arguments override environment variables, which override defaults.
import osfrom collections import ChainMap
def build_config(cli_args: dict) -> ChainMap: """ Build a configuration with three layers of priority. CLI args > environment variables > defaults """ defaults = { "host": "localhost", "port": "5432", "log_level": "INFO", "workers": "4", } env_vars = { "host": os.environ.get("DB_HOST", "localhost"), "port": os.environ.get("DB_PORT", "5432"), } # Only include env vars that were actually set env_vars = {k: v for k, v in env_vars.items() if v}
return ChainMap(cli_args, env_vars, defaults)
# Simulate CLI args provided by the usercli = {"port": "9000", "log_level": "DEBUG"}config = build_config(cli)
print(config["port"]) # "9000" — CLI winsprint(config["host"]) # from env or defaultsprint(config["log_level"]) # "DEBUG" — CLI winsprint(config["workers"]) # "4" — from defaultsWrites Go to the First Map Only
from collections import ChainMap
base = {"x": 1, "y": 2}overlay = {}
combined = ChainMap(overlay, base)combined["x"] = 100 # writes to overlay, not basecombined["z"] = 300 # new key goes to overlay
print(overlay) # {'x': 100, 'z': 300}print(base) # {'x': 1, 'y': 2} — unchanged
# Reading still falls through to base for unmodified keysprint(combined["y"]) # 2This is how ChainMap preserves the original dicts — writes create a shadow in the front map rather than modifying the underlying ones.
new_child() and parents
new_child() creates a new ChainMap with a fresh empty dict prepended. parents returns a ChainMap with the first map removed.
from collections import ChainMap
base_config = ChainMap({"timeout": 30, "retries": 3})
# Create a child scope with temporary overridestemp_config = base_config.new_child({"timeout": 5})print(temp_config["timeout"]) # 5 — from childprint(temp_config["retries"]) # 3 — from parent
# Leave the child scopeprint(temp_config.parents["timeout"]) # 30This makes ChainMap useful for simulating variable scoping — each nested scope is a child map, and resolving a name walks up the chain of parents.
ChainMap vs dict Update
from collections import ChainMap
a = {"x": 1}b = {"y": 2}
# dict merge — creates a new dict, originals unchanged but you lose layeringmerged = {**a, **b}
# ChainMap — no copy, lookup checks both, writes isolated to firstchained = ChainMap(a, b)
# Key difference: if you update 'a' later, ChainMap reflects it; merged does nota["z"] = 3print(merged) # {'x': 1, 'y': 2} — staleprint(chained) # sees z: 3 from a — live viewIterating a ChainMap
When iterating, you see each key once — the value from the highest-priority dict wins:
from collections import ChainMap
c = ChainMap({"a": 1, "b": 2}, {"b": 99, "c": 3})
for key, value in c.items(): print(key, value)# a 1# b 2 — from the first dict, not 99# c 3
print(list(c.keys())) # ['a', 'b', 'c']print(dict(c)) # {'a': 1, 'b': 2, 'c': 3}len(c) returns the number of unique keys across all maps. Duplicate keys are counted once.