Applications often combine several configuration sources: command-line arguments, environment variables, a local file, and built-in defaults. Copying everything into a new dictionary works, but it loses the origin of each value and must be repeated whenever a layer changes. collections.ChainMap presents multiple mappings as one view and searches them from the first layer to the last.
This guide explains precedence, reading and writing, the maps list, new_child(), parents, nested scopes, configuration layering, differences from dictionary union, performance, mutability, serialization, and safe design.
Your first ChainMap
from collections import ChainMap
defaults = {"theme": "light", "timeout": 30}
user = {"theme": "dark"}
config = ChainMap(user, defaults)
print(config["theme"]) # dark
print(config["timeout"]) # 30
Lookup visits mappings in the order supplied. The first occurrence wins. The original dictionaries are not copied.
A live view
defaults["timeout"] = 60
print(config["timeout"]) # 60
ChainMap stores references, so changes in any layer become visible immediately. This is useful for live configuration but surprising when callers expect an immutable snapshot.
Writes target the first map
config["timeout"] = 10
print(user)
# {'theme': 'dark', 'timeout': 10}
Assignments, deletions, pop(), and related mutable operations affect only the first mapping. Even if a key exists deeper in the chain, assigning it creates an override in the first layer.
Deleting a key
del config["theme"]
The deletion removes the key only from the first map. If the same key exists later, that value becomes visible. If the first map does not contain the key, deletion raises KeyError even when a lower layer contains it.
The maps list
print(config.maps)
maps is the actual list of mappings. You may inspect, append, or reorder layers, but structural changes modify precedence for every consumer sharing the ChainMap instance.
Four-level configuration
config = ChainMap(
cli_arguments,
environment_values,
config_file,
defaults,
)
Place the highest-priority source first. Parse types before building the chain: environment values are strings, while defaults may be integers, booleans, paths, or structured objects.
Difference from dictionary union
merged = defaults | config_file | environment_values | cli_arguments
The | operator creates a new dictionary resolved at that moment. ChainMap creates a dynamic view, retains layer boundaries, and avoids copying every entry. A materialized dictionary is simpler for JSON output, isolation, and reproducible execution.
Materializing a snapshot
snapshot = dict(config)
Convert to dict when you need a stable snapshot, comparison, serialization, or protection from later layer changes. The conversion is shallow: nested mutable values remain shared.
new_child
global_scope = ChainMap(globals_map)
function_scope = global_scope.new_child(locals_map)
new_child() creates another ChainMap with a new first mapping followed by the existing layers. If no mapping is supplied, an empty dictionary is created. This is useful for lexical scopes, temporary overrides, and nested contexts.
parents
parent_scope = function_scope.parents
parents returns a new view excluding the first mapping. It does not mutate the original chain or destroy data.
Modeling variable scopes
global_values = {"tax_rate": 0.1}
local_values = {"subtotal": 100}
scope = ChainMap(local_values, global_values)
Lookup checks local names before global names. Assignment writes locally and naturally models shadowing. Interpreters and template engines can use this pattern.
Temporary context
base = ChainMap(global_config)
temporary = base.new_child({"debug": True})
run(temporary)
The base does not need to be copied. Discarding the temporary view removes the override from use. Direct mutations to shared deeper maps still remain visible.
Iteration order
Iteration yields each visible key once. Its order resembles updating a dictionary from the last mapping toward the first, while value lookup searches first to last. Do not confuse display order with precedence.
Length and membership
len(config) counts unique visible keys rather than summing every map size. Membership may search several layers before finding a key.
def source_index(chain, key):
for index, mapping in enumerate(chain.maps):
if key in mapping:
return index
return None
This helper identifies the highest-priority layer defining a key.
Updating a specific layer
To modify a deeper mapping, access it directly:
config.maps[2]["timeout"] = 45
Wrap numeric positions in named objects or helper methods so code does not break when the layer order changes.
DeepChainMap semantics
Some applications want assignment to update the first layer where the key already exists:
class DeepChainMap(ChainMap):
def __setitem__(self, key, value):
for mapping in self.maps:
if key in mapping:
mapping[key] = value
return
self.maps[0][key] = value
This differs from the standard contract. Document the custom behavior clearly.
Read-only layers
MappingProxyType can protect deeper mappings from direct writes. The first map must still be mutable when consumers use ChainMap mutation methods.
Validation and schemas
ChainMap does not validate names or convert values. Parse every source, validate the resolved result, and define whether unknown keys are allowed. A present key can still have the wrong type or an unsafe value.
None as an override
A first layer containing {"timeout": None} hides a deeper timeout. Decide whether None means disabled, explicitly null, or “use the default.” Filter it before constructing the chain when it should mean absence.
Concurrency
ChainMap and its underlying dictionaries do not provide synchronization. Readers may observe intermediate states while another thread mutates layers. Prefer immutable snapshots or locks when configuration changes concurrently.
Serialization
JSON encoders do not generally serialize ChainMap directly. Use dict(chain) for resolved values or serialize chain.maps when source boundaries must be preserved. Redact passwords, tokens, and secret environment variables.
Performance
A value in the first map is found quickly; a value only in the last map requires checking every preceding layer. A few configuration layers are inexpensive. Hundreds of layers on hot paths should be flattened or redesigned.
ChainMap versus defaultdict
defaultdict creates missing values in one dictionary. ChainMap searches multiple existing mappings. They solve different problems and can be combined, although automatic creation in a first layer may hide lower values.
ChainMap versus contextvars
ChainMap models mapping precedence. contextvars propagates task-local state across asynchronous execution. Do not use one shared mutable ChainMap as a substitute for per-task context.
Common mistakes
- Expecting a copy: layers remain live references.
- Expecting assignment to update the original source: it writes to the first map.
- Deleting a lower-layer key through the chain: deletion affects the first map only.
- Ignoring source types: environment variables are strings.
- Treating iteration order as lookup precedence: they differ.
- Sharing mutations without synchronization: readers may observe inconsistent states.
Complete application configuration example
from collections import ChainMap
DEFAULTS = {
"host": "127.0.0.1",
"port": 8000,
"debug": False,
}
def build_config(cli, environment, file_values):
parsed_environment = {}
if "APP_PORT" in environment:
parsed_environment["port"] = int(environment["APP_PORT"])
if "APP_DEBUG" in environment:
parsed_environment["debug"] = (
environment["APP_DEBUG"].lower() == "true"
)
layers = ChainMap(cli, parsed_environment, file_values, DEFAULTS)
resolved = dict(layers)
if not 1 <= resolved["port"] <= 65535:
raise ValueError("invalid port")
return resolved
The function uses ChainMap for precedence and returns a validated snapshot for stable execution.
Conclusion
collections.ChainMap provides a lightweight view over layered mappings, preserving precedence and source boundaries without copying every entry. It is well suited to configuration, scopes, and temporary overrides.
The official Python ChainMap documentation defines the API. Order layers deliberately, materialize snapshots when isolation matters, and remember that mutation targets only the first mapping.







