Layer Settings with ChainMap
Build a shallow configuration snapshot with explicit precedence across three mappings.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Layer Settings with ChainMap
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Many programs accept settings from several places. A command-line option should usually override an environment setting, and an environment setting should override a built-in default. That sounds simple until code merges several dictionaries and accidentally gives the default the highest priority. Today we will build a tiny layered configuration using `collections.ChainMap`. It presents multiple mappings as one view and searches them from left to right. We will reproduce a wrong merge, inspect which layer wins, then add validation so a bad port cannot slip through as a string. We will also check what happens when one of the original dictionaries changes after
- 00:41
the `ChainMap` is created. That detail matters because `ChainMap` is a live view, not a frozen copy of all values. The examples are entirely local and work with ordinary Python dictionaries. You can edit the priorities and run the code on the lesson page. Keep the policy in mind: we want command line, then environment, then defaults. The loop starts with the strongest override and ends with defaults. But `dict.update` replaces existing keys with later values, so the last layer wins. By the end, the default port eight thousand has overwritten both higher- priority choices.
- 01:21
This is a straightforward bug because the source order appears to match the intended precedence when read from left to right. One repair would reverse the update order. That can be fine for a small immutable configuration, but it creates a merged copy and hides which mapping supplied a value. It also will not reflect later changes to an underlying layer. For this lesson, we want the lookup order to be explicit. The effective configuration should first check command-line overrides, then the environment dictionary, then defaults. The fact that the printed dictionary includes the default host does not make the port correct. Testing only that a key exists would miss this precedence mistake; we need a key
- 02:04
that appears in multiple layers. `ChainMap` stores references to the supplied mappings. A lookup checks them in the order we passed them. The first port is nine thousand because the command-line mapping has the key. The host falls through to defaults because the first two mappings have no host. Then we delete the command-line port from its original dictionary. The very next lookup yields eight thousand eighty from the environment layer. We did not rebuild the `ChainMap`; it is a view over live mappings. This is useful when settings should reflect an updated override, but it can surprise you if you expected a snapshot.
- 02:44
If you need an immutable configuration at program startup, copy and validate the effective values into a separate object. Another important rule: assignment through a `ChainMap` normally writes to its first mapping, not to whichever layer originally held the key. A reader may see a default, but a write of that key goes into the override layer. We will use that behavior deliberately in the final verification. The helper states the priority at its construction line: command line first, environment second, defaults last. It then converts the effective value to an integer and checks the valid port range.
- 03:24
The command-line and environment examples arrive as strings, which is common when reading arguments or environment variables. Conversion happens after precedence resolution, so we validate the value that the application will actually use. We also provide a specific error message when conversion fails and chain the original exception for debugging. A real application might choose a dataclass or a typed configuration object to store the final validated settings. This small function returns only the port to keep the demonstration focused. It is important not to treat `ChainMap` itself as a validation framework; it only defines lookup order.
- 04:06
If the first layer contains an invalid value, it does not automatically skip it and try a lower-priority layer. That would hide a user mistake. We reject the invalid override instead of silently falling back to a different port. The first assertion finds the environment port because there is no command-line override yet. Assignment through `config` writes into the first mapping, so the command-line dictionary now contains nine thousand while the environment dictionary remains unchanged. Deleting that new key reveals the environment port again. This sequence is a direct test of the live view behavior, not merely of the value returned in one static case.
- 04:49
The final conversion demonstrates that an invalid string should trigger a visible error. In a complete application, the validation helper from the previous chapter would handle that error. Try inserting a command-line value of zero and predict whether a lookup or the validator will reject it. `ChainMap` will happily return zero; the validator must enforce the port range. Try changing the order of the mappings and observe how the effective value changes. When you finish the interactive task, you should be able to state both rules: earlier mappings have higher lookup priority, and writes through a normal `ChainMap` target its first mapping.
- 05:30
Use a copy when you need a frozen, validated snapshot rather than a live layered view.
Understand the concept
An application may read defaults, environment settings, and per-run overrides. The same key can appear in more than one source. collections.ChainMap creates a view across mappings and searches the first mapping, then the next. Put the highest-priority source first so a caller's explicit choice can win.
ChainMap(cli, environment, defaults) gives command-line values priority, then environment, then defaults. It does not copy the dictionaries. A later change in one source can change the view, which is useful for some programs and surprising for others. Returning dict(view) gives callers an independent top-level snapshot.
This is a shallow merge. A nested dictionary is one value; ChainMap does not merge keys inside it. For example, overriding a db mapping that has only a host will hide the default db mapping's port. If a product needs recursive merging, write and test that separate policy rather than assuming ChainMap provides it.
Writes to a ChainMap target its first mapping by default. Do not expose a live view to code that should only read settings. The task returns a plain dict, never edits inputs, and checks duplicate and missing keys. It layers already-parsed mappings; reading an .ini file or environment variables is a different step.
- ChainMap searches mappings left to right
- A view reflects later source changes
- dict(view) creates a shallow snapshot
- Nested dictionaries are not recursively merged
See it step by step
Read the code, predict the output, then compare it with the result.
01. See precedence at a glance
from collections import ChainMap
view = ChainMap({'timeout': 5}, {'timeout': 12}, {'timeout': 30, 'host': 'local'})
print(view['timeout'], view['host'])5 localThe first mapping that has a key supplies it.
02. Notice that a ChainMap is a live view
from collections import ChainMap
cli = {'timeout': 5}
view = ChainMap(cli, {'timeout': 30})
cli['timeout'] = 8
print(view['timeout'])8The view reflects a later change to its source mapping.
03. Freeze values for a caller
from collections import ChainMap
cli = {'timeout': 5}
snapshot = dict(ChainMap(cli, {'host': 'local'}))
cli['timeout'] = 8
print(snapshot['timeout'], snapshot['host'])5 localA plain dict is a separate shallow snapshot of effective top-level values.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Put the highest-priority mapping first
The first ChainMap contains three mappings with repeated timeout keys. It returns five from the first mapping, while host comes from the last mapping because earlier layers lack that key. This is top-level lookup precedence, so the order in which sources are passed is the policy.
For a typical configuration, place command-line overrides first, environment-derived settings second, and defaults last. Do not reverse them by accident: a later default should not defeat an explicit user choice. The checked function takes separate mappings to make that priority visible and verifies both conflicts and keys found only in lower-priority layers.
from collections import ChainMap
view = ChainMap({'timeout': 5}, {'timeout': 12}, {'timeout': 30, 'host': 'local'})
print(view['timeout'], view['host'])5 local- Create layers with a duplicate timeout key.
- Read the first matching value and a fallback key.
- Document the priority order at the call site.
Observe why a ChainMap is a live view
The next example mutates the original cli dictionary after constructing the ChainMap. Reading timeout again returns eight, not five. A ChainMap stores references to its mappings; it does not freeze their top-level values when created. That can be useful for a dynamic view and surprising for a function that promises stable settings.
If a caller needs a snapshot, convert the effective view to a plain dict before returning it. The exercise does not edit its input mappings. Test by changing the source after calling the function and checking that the returned top-level value stays the same. Remember that this is still a shallow copy: nested mutable objects remain shared.
from collections import ChainMap
cli = {'timeout': 5}
view = ChainMap(cli, {'timeout': 30})
cli['timeout'] = 8
print(view['timeout'])8- Read a value through the live view.
- Change the original dictionary and read it again.
- Use dict(view) when a top-level snapshot is required.
Freeze effective top-level settings
The last run converts ChainMap to a dict before mutating the source. The printed timeout remains five, while host is inherited from the lower layer. That proves a separate dictionary holds the effective top-level choices at snapshot time. It does not merge nested dictionaries field by field.
A common mistake is expecting ChainMap to recursively combine two db dictionaries. It treats each db mapping as one value, so the first wins entirely. If recursive merging is required, define and test that policy separately. For this lesson, keep the contract narrow: never change inputs, return a shallow effective snapshot, and verify precedence with duplicate keys.
from collections import ChainMap
cli = {'timeout': 5}
snapshot = dict(ChainMap(cli, {'host': 'local'}))
cli['timeout'] = 8
print(snapshot['timeout'], snapshot['host'])5 local- Create a ChainMap in priority order.
- Convert the view to a dict before returning it.
- Test mutation of the source and a nested-value limitation.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Use dict(ChainMap(cli, environment, defaults)).
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Reversing the priority order
ChainMap(defaults, environment, cli)ChainMap(cli, environment, defaults)What happens: A default hides an explicit override.
Place the highest-priority source first.
Expecting a deep merge
ChainMap({'db': {'host':'new'}}, {'db': {'port':5432}})['db']['port']Define a nested merge policy, or use flat keysWhat happens: The top db mapping lacks port and raises KeyError.
ChainMap combines top-level lookups, not nested values.