Python ChainMap Scope Lookup Rules for Duplicate Keys
Python's collections.ChainMap groups multiple
dictionaries or mappings into a single updateable view using a linear,
priority-based search mechanism. When identical keys exist across
multiple constituent maps, ChainMap resolves lookups by
scanning its internal list of mappings from left to right, returning the
value from the first map containing that key and ignoring all subsequent
duplicates. This design explicitly mirrors Python's lexical scoping
resolution, providing an efficient way to simulate nested contexts such
as local, global, and default environments.
The Left-to-Right Lookup Order
At its core, a ChainMap stores an ordered list of
mappings accessible via its .maps attribute. When a key
lookup occurs (such as chain[key]), ChainMap
executes an internal search that iterates sequentially through
self.maps:
- It checks the first mapping (
self.maps[0]). If the key is present, it immediately returns the associated value. - If the key is absent in the first mapping, it proceeds to the second
mapping (
self.maps[1]), and continues onward. - If the key is not found in any mapping in the chain, it raises a
standard
KeyError.
Because the lookup terminates at the very first occurrence, the entry in the leftmost map effectively shadows (or masks) identical keys in all deeper layers.
Read vs. Write Asymmetry
While read operations traverse the entire chain until a key is located, write operations behave differently:
- Key Assignment (
chain[key] = value): Always targetsself.maps[0]. It creates or updates the key in the first map exclusively, leaving all underlying maps unchanged. - Key Deletion (
del chain[key]): Attempts to delete the key only fromself.maps[0]. If the key does not exist in the first map, Python raises aKeyError, even if the key exists in an underlying map. - Mutating Methods (
pop(),clear()): Similar to deletion, methods that remove or clear items only operate on the primary mapping.
This asymmetry ensures that child contexts cannot inadvertently corrupt or overwrite inherited configuration or parent scopes.
Emulating
Variable Scope with new_child and parents
ChainMap includes dedicated methods designed to model
entering and exiting nested scopes:
new_child(m=None): Creates a newChainMapcontaining an empty dictionary (or the provided mappingm) prepended to the beginning of the existing maps list. This newly added map becomes the topmost scope, allowing new values to override existing ones without modifying the parent scopes.parents: Returns a newChainMapconsisting of all mappings except the first (self.maps[1:]). This represents the enclosing or outer scope, effectively bypassing local overrides.
Practical Example
from collections import ChainMap
# Define distinct scopes from highest to lowest priority
local_scope = {"port": 8080}
override_scope = {"host": "127.0.0.1", "port": 5000}
default_scope = {"host": "localhost", "port": 80, "debug": True}
# Initialize ChainMap
config = ChainMap(local_scope, override_scope, default_scope)
# Duplicate key 'port' exists in all three scopes:
# Resolves to 8080 because local_scope is checked first
print(config["port"]) # Output: 8080
# 'host' is missing in local_scope, so override_scope is used
print(config["host"]) # Output: 127.0.0.1
# 'debug' is only in default_scope
print(config["debug"]) # Output: True
# Writing a new value only affects the first dictionary
config["timeout"] = 30
print(local_scope) # Output: {'port': 8080, 'timeout': 30}
print(override_scope) # Output: {'host': '127.0.0.1', 'port': 5000}Through this sequential lookup mechanism and localized mutation
strategy, collections.ChainMap provides predictable scope
hierarchies that prioritize high-precedence settings while maintaining
fallback defaults.