A ChainMap is a data structure in the collections module that allows for multiple mappings( mostly dictionaries) to be linked together and treated like a single unit. 

In a single ChainMap multiple dictionaries are connected in a chain-like manner. This allows for efficient lookup of values, since each dictionary is searched in turn, going from the first to the last. When a key is found in a dictionary, the corresponding value is returned and the search is terminated. If the key is not found in any of the dictionaries, an exception is raised. 

The ChainMap keeps references to the original dictionaries, this means that  if any of the underlying dictionaries are modified, those changes will be reflected in the ChainMap.

Instantiating ChainMaps

To create a ChainMap, we start by importing the class from the collections module then use the following syntax.

Syntax:
ChainMap(*maps)
copy

Where the parameter maps represents the arbitrary dict objects(or other mappings).

ExampleEdit & Run

Create a ChainMap

#import the ChainMap class
from collections import ChainMap

d1 = {'one': 1, 'two': 2, 'three': 3}
d2 = {'four': 4, 'five': 5, 'six': 6}
d3 = {'seven': 7, 'eight': 8, 'nine': 9}

chain = ChainMap(d1, d2, d3)

print(chain)
copy
Output:
ChainMap({'one': 1, 'two': 2, 'three': 3}, {'four': 4, 'five': 5, 'six': 6}, {'seven': 7, 'eight': 8, 'nine': 9}) [Finished in 0.012535670772194862s]

Any modifications done to any of the linked dictionaries will be reflected in the ChainMap.

ExampleEdit & Run
#import the ChainMap class
from collections import ChainMap

d1 = {'one': 1, 'two': 2, 'three': 3}
d2 = {'four': 4, 'five': 5, 'six': 6}
d3 = {'seven': 7, 'eight': 8, 'nine': 9}

chain = ChainMap(d1, d2, d3)

d1['zero'] = 0

print(chain)
copy
Output:
ChainMap({'one': 1, 'two': 2, 'three': 3, 'zero': 0}, {'four': 4, 'five': 5, 'six': 6}, {'seven': 7, 'eight': 8, 'nine': 9}) [Finished in 0.011941061355173588s]

Add a new dictionary to the Chainmap

The new_child() method adds a new dictionary at the beginning of the chain and returns a new ChainMap reflecting the change.

ExampleEdit & Run
#import the ChainMap class
from collections import ChainMap

d1 = {'one': 1, 'two':2}
d2 = {'three': 3, 'four': 4}
chain = ChainMap(d1, d2)

newd = {'ten': 10, 'twenty': 20}

chain  = chain.new_child(newd)
print(chain)
copy
Output:
ChainMap({'ten': 10, 'twenty': 20}, {'one': 1, 'two': 2}, {'three': 3, 'four': 4}) [Finished in 0.011981690768152475s]

Accessing values by their keys

When we access an element from the ChainMap the element is looked up in the first map in the chain, if it is not found, then the lookup is continued in the next map in the chain (if any). This process continues until the element is found in one of the maps. If the element is not found in any of the maps, then just like in dictionaries, a KeyError is raised.

ExampleEdit & Run
#import the ChainMap class
from collections import ChainMap

d1 = {'one': 1, 'two': 2, 'three': 3}
d2 = {'four': 4, 'five': 5, 'six': 6}
d3 = {'seven': 7, 'eight': 8, 'nine': 9}

chain = ChainMap(d1, d2, d3)

print(chain['three'])
print(chain['five'])
print(chain['eight'])
print(chain['nine'])
copy
Output:
3 5 8 9 [Finished in 0.01187832374125719s]

In the above example, we used the bracket notation to access some values. This approach will raise a KeyError if an item with the given key does not exist in any of the dictionaries.

ExampleEdit & Run
#import the ChainMap class
from collections import ChainMap

d1 = {'one': 1, 'two': 2, 'three': 3}
d2 = {'four': 4, 'five': 5, 'six': 6}
d3 = {'seven': 7, 'eight': 8, 'nine': 9}

chain = ChainMap(d1, d2, d3)

print(chain['ten']) #non-existent
copy
Output:
Traceback (most recent call last):   File "<string>", line 10, in <module>   File "/app/.heroku/python/lib/python3.11/collections/__init__.py", line 1006, in __getitem__     return self.__missing__(key)            # support subclasses that define __missing__            ^^^^^^^^^^^^^^^^^^^^^   File "/app/.heroku/python/lib/python3.11/collections/__init__.py", line 998, in __missing__     raise KeyError(key) KeyError: 'ten' [Finished in 0.011866431217640638s]

In the above example, the key "ten" does not exist in the ChainMap leading to the exception being raised.

The get() method is the functional equivalent to the brackets notation. It allows us to access a value whose key is given as argument. This approach has an advantage in that it allows us to specify a default value that will be returned in case the key does not exist in any of the linked dictionaries, this allows us to avoid the KeyError exception without using extra exception handling.

ExampleEdit & Run
#import the ChainMap class
from collections import ChainMap

d1 = {'one': 1, 'two': 2, 'three': 3}
d2 = {'four': 4, 'five': 5, 'six': 6}
d3 = {'seven': 7, 'eight': 8, 'nine': 9}

chain = ChainMap(d1, d2, d3)

print(chain.get('two'))
print(chain.get('six'))
print(chain.get('seven'))
print(chain.get('ten', 'not found'))
copy
Output:
2 6 7 not found [Finished in 0.01173889497295022s]

In the above example, we used the get() method to access values by their keys. Note that in the last statement, the key 'ten' does not exist, we provided a default value i.e 'not found' which is returned instead of raising the KeyError exception.

Inserting and removing and modifying items

Removal, insertion and modification operations only happens to the first dictionary in the ChainMap. This is very important to note as it can have some some unexpected outcomes. Consider the following example

ExampleEdit & Run
#import the ChainMap class
from collections import ChainMap

d1 = {'one': 1, 'two': 2}
d2 = {'four': 4, 'five': 5}
d3 = {'two': 7, 'eight': 8}

chain = ChainMap(d1, d2, d3)

chain['thirty'] = 30
chain['eight'] = 8000
print(chain)

print(d1)
copy
Output:
ChainMap({'one': 1, 'two': 2, 'thirty': 30, 'eight': 8000}, {'four': 4, 'five': 5}, {'two': 7, 'eight': 8}) {'one': 1, 'two': 2, 'thirty': 30, 'eight': 8000} [Finished in 0.0115657071582973s]

Note that in the above example,  chain['thirty'] =30 led the new item to be added to the first dictionary. But wait, what about chain['eight'] = 8000 ? in normal situation we would have expected that this operation to just update the item with key 'eight' in the third dictionary, however, the item was added as a new item to the first dictionary as well.

Trying to remove an item existing in other dictionaries(other than the first in the ChainMap) will lead to a KeyError. Consider the following example:

ExampleEdit & Run
#import the ChainMap class
from collections import ChainMap

d1 = {'one': 1, 'two': 2, 'three': 3}
d2 = {'four': 4, 'five': 5, 'six': 6}
d3 = {'two': 7, 'eight': 8, 'nine': 9}

chain = ChainMap(d1, d2, d3)

del chain['two']
del chain['five']
print(chain)
copy
Output:
Traceback (most recent call last):   File "/app/.heroku/python/lib/python3.11/collections/__init__.py", line 1062, in __delitem__     del self.maps[0][key]         ~~~~~~~~~~~~^^^^^ KeyError: 'five' During handling of the above exception, another exception occurred: Traceback (most recent call last):   File "<string>", line 11, in <module>   File "/app/.heroku/python/lib/python3.11/collections/__init__.py", line 1064, in __delitem__     raise KeyError(f'Key not found in the first mapping: {key!r}') KeyError: "Key not found in the first mapping: 'five'" [Finished in 0.012060326989740133s]

As shown above, the key 'two' was removed successfully but 'five' lead to a KeyError saying that the key does not exist. 

Due to the inconveniences shown above, it is better to perform operations  that modify the ChainMap directly on the dictionaries themselves rather than through the ChainMap, the change will be reflected in the ChainMap anyway.

ExampleEdit & Run
#import the ChainMap class
from collections import ChainMap

d1 = {'one': 1, 'two': 2, 'three': 3}
d2 = {'four': 4, 'five': 5, 'six': 6}
d3 = {'two': 7, 'eight': 8, 'nine': 9}

chain = ChainMap(d1, d2, d3)

del d1['two']
del d2['five']
del d3['nine']
print(chain)
copy
Output:
ChainMap({'one': 1, 'three': 3}, {'four': 4, 'six': 6}, {'two': 7, 'eight': 8}) [Finished in 0.011220097076147795s]

ChainMap information

The ChainMap class defines the items()keys(), and  values() methods which returns  the items, the keys and the values in the ChainMap, respectively.

ExampleEdit & Run
from collections import ChainMap

Europe = {'Germany': 'Berlin', 'France': 'Paris', 'Spain': 'Madrid'}
Africa = {'Nairobi': 'Kenya', 'Rwanda': 'Kigali', 'Nigeria': 'Abuja'}
Asia = {'Japan': 'Tokyo', 'China': 'Beijing', 'India': 'Delhi'}

chain = ChainMap(Europe, Africa, Asia)

print("items:")
print(chain.items(), '\n')

print("keys:")
print(chain.keys(), '\n')

print("values:")
print(chain.values(), '\n')
copy
Output:
items: ItemsView(ChainMap({'Germany': 'Berlin', 'France': 'Paris', 'Spain': 'Madrid'}, {'Nairobi': 'Kenya', 'Rwanda': 'Kigali', 'Nigeria': 'Abuja'}, {'Japan': 'Tokyo', 'China': 'Beijing', 'India': 'Delhi'}))  keys: KeysView(ChainMap({'Germany': 'Berlin', 'France': 'Paris', 'Spain': 'Madrid'}, {'Nairobi': 'Kenya', 'Rwanda': 'Kigali', 'Nigeria': 'Abuja'}, {'Japan': 'Tokyo', 'China': 'Beijing', 'India': 'Delhi'}))  values: ValuesView(ChainMap({'Germany': 'Berlin', 'France': 'Paris', 'Spain': 'Madrid'}, {'Nairobi': 'Kenya', 'Rwanda': 'Kigali', 'Nigeria': 'Abuja'}, {'Japan': 'Tokyo', 'China': 'Beijing', 'India': 'Delhi'}))  [Finished in 0.011478336993604898s]