LESSON 89 · DEEPER PYTHON29 min

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

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 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

  2. 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.

  3. 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

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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

PYTHON
from collections import ChainMap
view = ChainMap({'timeout': 5}, {'timeout': 12}, {'timeout': 30, 'host': 'local'})
print(view['timeout'], view['host'])
EXPECTED OUTPUT
5 local

The first mapping that has a key supplies it.

02. Notice that a ChainMap is a live view

PYTHON
from collections import ChainMap
cli = {'timeout': 5}
view = ChainMap(cli, {'timeout': 30})
cli['timeout'] = 8
print(view['timeout'])
EXPECTED OUTPUT
8

The view reflects a later change to its source mapping.

03. Freeze values for a caller

PYTHON
from collections import ChainMap
cli = {'timeout': 5}
snapshot = dict(ChainMap(cli, {'host': 'local'}))
cli['timeout'] = 8
print(snapshot['timeout'], snapshot['host'])
EXPECTED OUTPUT
5 local

A 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.

01 / 03

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.

PYTHON
from collections import ChainMap
view = ChainMap({'timeout': 5}, {'timeout': 12}, {'timeout': 30, 'host': 'local'})
print(view['timeout'], view['host'])
EXPECTED OUTPUT
5 local
Follow the reasoning
  1. Create layers with a duplicate timeout key.
  2. Read the first matching value and a fallback key.
  3. Document the priority order at the call site.
02 / 03

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.

PYTHON
from collections import ChainMap
cli = {'timeout': 5}
view = ChainMap(cli, {'timeout': 30})
cli['timeout'] = 8
print(view['timeout'])
EXPECTED OUTPUT
8
Follow the reasoning
  1. Read a value through the live view.
  2. Change the original dictionary and read it again.
  3. Use dict(view) when a top-level snapshot is required.
03 / 03

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.

PYTHON
from collections import ChainMap
cli = {'timeout': 5}
snapshot = dict(ChainMap(cli, {'host': 'local'}))
cli['timeout'] = 8
print(snapshot['timeout'], snapshot['host'])
EXPECTED OUTPUT
5 local
Follow the reasoning
  1. Create a ChainMap in priority order.
  2. Convert the view to a dict before returning it.
  3. 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 run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnReturn a plain dictionary with cli taking priority, then environment, then defaults. Do not mutate inputs. The prepared comparison must print True.
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.

    01.Which mapping wins in ChainMap(cli, env, defaults)?

    02.Does ChainMap recursively merge nested dictionaries?

    03.Why return dict(view) in this task?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Reversing the priority order

    COMMON MISTAKE
    ChainMap(defaults, environment, cli)
    FIX
    ChainMap(cli, environment, defaults)

    What happens: A default hides an explicit override.
    Place the highest-priority source first.

    Expecting a deep merge

    COMMON MISTAKE
    ChainMap({'db': {'host':'new'}}, {'db': {'port':5432}})['db']['port']
    FIX
    Define a nested merge policy, or use flat keys

    What happens: The top db mapping lacks port and raises KeyError.
    ChainMap combines top-level lookups, not nested values.

    Browse this lesson’s problem fixes

    Sources and further reading