ChainMap for Layered Configs
`collections.ChainMap` lets you stack multiple dicts and treat them as a single read-through view, with later dicts shadowing earlier ones. It is the right primitive for layered configs (defaults, environment, user overrides), nested scopes, and anywhere you would otherwise merge dicts repeatedly. This snippet covers the basic stacking, lookup-with-fallback semantics, and how new keys land in the first map by default.
452 views
10
from collections import ChainMap
defaults = {'theme': 'light', 'lang': 'en', 'page_size': 20}
user = {'lang': 'fr', 'page_size': 50}
request = {'page_size': 100}
config = ChainMap(request, user, defaults)
print(config['theme']) # 'light' (only in defaults)
print(config['lang']) # 'fr' (user overrides defaults)
print(config['page_size']) # 100 (request overrides everything)
print(list(config)) # all keys, deduplicated by first occurrenceChainMap looks up keys by walking its maps in order, returning the first match. So ChainMap(request, user, defaults) reads as 'request takes priority, then user, then defaults'. This is the cleanest expression of layered configuration in the Python standard library. Iterating the chain returns each unique key exactly once, in first-occurrence order. The chain holds references to the underlying dicts (no copy), so updating any layer is immediately visible through the view.
from collections import ChainMap
defaults = {'theme': 'light', 'lang': 'en'}
overrides = {'lang': 'fr'}
config = ChainMap(overrides, defaults)
# Writes go to the FIRST map only.
config['theme'] = 'dark'
print(overrides) # {'lang': 'fr', 'theme': 'dark'}
print(defaults) # {'theme': 'light', 'lang': 'en'} (untouched)
# Deletion also targets the first map; deleting from a deeper map is not allowed.
del config['lang']
print(overrides) # {'theme': 'dark'}
print(config['lang']) # 'en' (now visible because the override is gone)Mutations on a ChainMap (write or delete) only affect the FIRST map in the chain. Deleting a key that exists deeper raises KeyError. This is what makes the chain safe to use as a per-request scope: writes inside the request never accidentally update shared defaults. The new_child() helper (config.new_child(extra)) creates a fresh chain with the new dict prepended, which is the canonical way to push a temporary scope without mutating the existing chain.
from collections import ChainMap
def interpret(scope, body):
for line in body:
op, *args = line.split()
if op == 'set':
scope[args[0]] = args[1]
elif op == 'get':
print(args[0], '=', scope.get(args[0]))
elif op == 'enter':
scope = scope.new_child()
elif op == 'leave':
scope = scope.parents
global_scope = ChainMap()
interpret(global_scope, [
'set x 1',
'enter',
'set x 2',
'get x', # x = 2 (inner scope wins)
'leave',
'get x', # x = 1 (back in outer scope)
])ChainMap's new_child() and parents attribute model nested scopes the way a programming-language interpreter does. Entering a scope is scope = scope.new_child(); leaving it is scope = scope.parents (which returns a chain of all but the first map). Writes inside a child scope shadow the parent without touching it; leaving the scope discards the child entirely. This is also how Python's own scope rules can be modelled in a teaching interpreter or a small DSL.
