Technology  /  Python

🐍 Python 78 guides · updated 2026

From first variable to OOP, generators, and real projects — the language that runs everything from data pipelines to AI agents, taught the practical way.

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 → defaults
config = ChainMap(session_overrides, user_settings, defaults)
print(config["color"]) # "red" — from user_settings
print(config["size"]) # "medium" — from defaults
print(config["debug"]) # True — from session_overrides

The 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 os
from 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 user
cli = {"port": "9000", "log_level": "DEBUG"}
config = build_config(cli)
print(config["port"]) # "9000" — CLI wins
print(config["host"]) # from env or defaults
print(config["log_level"]) # "DEBUG" — CLI wins
print(config["workers"]) # "4" — from defaults

Writes 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 base
combined["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 keys
print(combined["y"]) # 2

This 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 overrides
temp_config = base_config.new_child({"timeout": 5})
print(temp_config["timeout"]) # 5 — from child
print(temp_config["retries"]) # 3 — from parent
# Leave the child scope
print(temp_config.parents["timeout"]) # 30

This 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 layering
merged = {**a, **b}
# ChainMap — no copy, lookup checks both, writes isolated to first
chained = ChainMap(a, b)
# Key difference: if you update 'a' later, ChainMap reflects it; merged does not
a["z"] = 3
print(merged) # {'x': 1, 'y': 2} — stale
print(chained) # sees z: 3 from a — live view

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