Skip to content
← All Python articles

Python explainer

Layer Configuration with ChainMap

About 4 min to read. Allow extra time to work through examples.

Look up a setting across default, environment and command-line layers without copying them, and know which layer a write changes.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your app reads its settings from three places. Defaults sit in the code, a few come from environment variables, and the command line can override anything. You could merge them into one dict. A collections.ChainMap keeps the layers separate instead, and searches them in order each time you look up a key.

from collections import ChainMap

defaults: dict[str, object] = {"port": 8000, "host": "localhost"}
options: dict[str, object] = {"port": 9000}
config = ChainMap(options, defaults)
assert config.get("port") == 9000
assert config["host"] == "localhost"

options["port"] = 9100
assert config.get("port") == 9100

Put the highest-priority layer first

A lookup checks config.maps, the list of layers, from first to last, and returns the first match. That is the reverse of a merge such as {**defaults, **options}, where the last value wins.

A ChainMap holds references to your dicts and copies nothing. So a change to a layer shows up in the chain at once, as the last line above shows.

Because the layers stay separate, you can also find out which layer a value came from. Loop over config.maps and stop at the first layer that contains the key. config[key] alone gives you the value but not the layer. If you log where each setting came from, keep a label with each layer, and leave secret values out of the log.

Writes and deletes change only the first layer

from collections import ChainMap

defaults: dict[str, object] = {"port": 8000}
options: dict[str, object] = {"port": 9000}
config = ChainMap(options, defaults)

config["port"] = 9200
assert options["port"] == 9200
assert defaults["port"] == 8000

del config["port"]
assert config["port"] == 8000

Setting or deleting a key through the chain changes the first layer only. Deleting an override uncovers the value underneath. Deleting a key that exists only in a later layer raises KeyError, because del looks only in the first layer.

The later layers are not protected. You can still change them through your own references to them, or through config.maps.

Check your understanding How can an application hide a value that a lower layer sets? Show answer

Decide on a rule and apply it in your own code. For example, an override can hold a special marker value that your code reads as "disabled". ChainMap has no such rule built in. Its del removes the key from the first layer only, not from every layer.

Add a temporary layer

from collections import ChainMap

defaults: dict[str, object] = {"port": 8000}
config: ChainMap[str, object] = ChainMap({}, defaults)

inner = config.new_child()
inner["port"] = 7000
assert inner["port"] == 7000
assert config["port"] == 8000
assert inner.parents["port"] == 8000

The new_child() method returns a new chain with a fresh empty dict in front of the existing layers. The parents property returns a new chain without the first layer. Neither one changes the original chain, and all of them share the layers underneath. That makes new_child handy for a short-lived override, such as the settings for one request.

Choose a live view or a snapshot

Use ChainMap when changes to the layers should show up at once, or when you need to inspect the layers one by one. Use a merged dict when you want a snapshot that later changes do not affect. A lookup in a chain may check several dicts. A lookup in a merged dict checks one.

A merge copies the entries, not the values in them. If a value is a list, the source and the merged dict share that one list.

Check your understanding Does ChainMap check or convert configuration values? Show answer

No. A ChainMap[str, object] has string keys and values of any type, and your code has to check each value before it uses it. Parse and validate the settings yourself. Then pass the rest of your code a typed settings object, if that makes it simpler.

Going further

When you loop over a ChainMap, it lists the keys by scanning the layers from last to first. So a key's position in that loop does not tell you which layer won. The ChainMap documentation has more examples, including one that mimics nested scopes.

step 9 of 55

Everyday Standard Library Tools

Up next Resolve a Setting and Report the Layer It Came From