Skip to content

← Stdlib Mastery step 8 of 55

Medium Primitives

ChainMap: resolve config and keep the provenance

Resolve a key across layered configuration without flattening the layers, and report which layer the value came from.

def solve(
    layers: list[dict[str, object]],
    key: str,
    patch_layer: int,
    patch_value: object,
    write_through: object,
) -> tuple[object, int, object, int, list[dict[str, object]]]:

layers is ordered lowest precedence first — the natural reading order [defaults, env, cli]. The last layer wins. ChainMap is the other way round (maps[0] wins), so you have to reverse on the way in and map the answer back to an index into the original list.

Do this, in order:

  1. Build one ChainMap over layers.
  2. Look the key up: the value, and the index into layers of the layer it came from. If no layer has the key, use "<missing>" and -1.
  3. If patch_layer >= 0, assign layers[patch_layer][key] = patch_value — mutating the source dict, not the chain.
  4. If write_through is not None, assign config["written"] = write_through through the ChainMap.
  5. Look the key up again with the same ChainMap.
  6. Return (before_value, before_index, after_value, after_index, layers).

The production consequence. {**defaults, **env, **cli} copies every key of every layer and produces a mapping that cannot answer “which layer set this?” — the first question asked in an incident review when staging and production disagree. It is also a snapshot: a later change to a source dict is invisible. Step 3 above is exactly that test. A solution that flattens once will report a stale value and fail.

The asymmetry you are proving in step 4. Writes through a ChainMap only ever touch maps[0]. Because you reversed, maps[0] is the last element of layers — so "written" must appear in layers[-1] and nowhere else. This is the whole design: the first map is the mutable scratch layer, everything behind it is read-only backing, which is precisely how a scope stack behaves (new_child() / .parents is the same API applied to nonlocal).

Your submission must pass mypy --strict. Annotate the chain (ChainMap[str, object]); values arriving from untyped configuration are object, and config.maps is a real list you can index.

Returns a 5-tuple, not a list. The harness compares container types exactly.