We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Edge of the System step 5 of 12
The configuration boundary in practice: file, environment, argv, one Config
Every previous item in this track handled one layer. Production has four, and the bugs live in the seams between them: a value set in the file, overridden in the environment by a leftover variable from a debugging session six weeks ago, and nobody can tell you why the service is listening on port 9100.
So the deliverable is not just a Config — it is a Config plus
provenance.
def resolve_config(file_table: Mapping[str, object],
env: Mapping[str, str],
argv: Sequence[str]) -> Resolved: ...
Resolved carries the frozen Config and a sources mapping saying which
layer won each field: "default", "file", "env" or "cli". That mapping is
the difference between a five-minute diagnosis and an afternoon. It costs you
one dictionary.
The precedence order, and why it is that order
defaults < file < environment < command line
Each layer is more specific to this run than the one before it. Defaults ship with the code. The file ships with the deployment. The environment is the container. The command line is this invocation. Reverse any pair and you have built something that cannot be overridden in an incident.
The resolution is per field, not per layer. A file that sets host does not
reset port to its default. Implement it as an overwrite in order — the last
writer wins, and you record who it was.
The three layers do not have the same shape
This is the part that catches people, and it is why one generic coerce() is
the wrong design:
| layer | what you receive | what you do |
|---|---|---|
file (TOML via tomllib) |
already-typed int, bool, str |
check the type — a "9000" in the file is a bug in the file |
| environment |
always str |
convert, and reject what does not convert |
| command line |
always str |
convert, same rules |
tomllib.load returns dict[str, Any], which passes --strict while being
completely unchecked — the exact trap from the tomllib item. Typing the
parameter Mapping[str, object] instead of Mapping[str, Any] is what forces
you to narrow every value before it reaches the dataclass.
(Two tomllib details worth carrying: it requires binary mode, because TOML
is UTF-8 and the parser owns the decoding; and it has no writing support —
reach for tomli-w or tomlkit.)
Unknown keys: the rule flips between layers
An unknown key in your config file is a typo — reject it. An unknown variable
in the environment is PATH, and rejecting it is absurd.
The reconciliation is a prefix. Every variable you own starts with APP_,
so APP_TIEMOUT is unambiguously a typo in your namespace and PATH is
unambiguously not yours. Namespacing is what buys back the “unknown key is an
error” property in a shared namespace. Do the same on the command line: an
unrecognised --flag is an error, never a silent no-op.
Command-line grammar
Keep it small and total:
-
--key=valuefor every field -
--verbose/--no-verbosefor boolean fields only -
anything else — a bare
--port, a positional argument, an unknown name — raises
Underscores in field names are written as dashes on the command line
(--workers, and --some-field for some_field).
Errors
raise ConfigError(layer, key, kind)
# layer in {"file", "env", "cli"}
# kind in {"unknown", "type", "malformed"}
key is the key as that layer spells it — tiemout for the file,
APP_TIEMOUT for the environment, --tiemout=30 for the command line. An
operator should be able to paste it straight back into the thing they need to
fix.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.