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:

  1. It checks the first mapping (self.maps[0]). If the key is present, it immediately returns the associated value.
  2. If the key is absent in the first mapping, it proceeds to the second mapping (self.maps[1]), and continues onward.
  3. 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:

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:

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.