We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Edge of the System step 1 of 12
Parse, don't validate: the boundary returns a narrower type
def validate(d: dict) -> dict is the single most common shape at a
system boundary, and it is wrong in a way that no amount of care inside the
function can fix: the caller cannot tell validated data from unvalidated
data, because both have the same type. Six months later someone calls the
inner function directly, the check never runs, and the failure surfaces three
layers away as a KeyError in a request handler.
The fix is a type-level statement:
A boundary function’s signature is
Mapping[str, object] -> Config.
After it returns you hold a Config. There is no code path that produces a
Config without going through the parse. The type is the proof. This is the
idea the rest of this track is built on — every injection class later in T13 is
the same mistake at a different boundary.
What you are building
def load_config(raw: Mapping[str, object], *, env: Mapping[str, str]) -> Config: ...
Config is a frozen, slotted dataclass:
| field | type |
|---|---|
name |
str |
port |
int |
debug |
bool |
database |
Database (url: str, pool_size: int) |
features |
tuple[str, ...] |
token |
Secret |
The dataclasses, Secret, ConfigError and the solve driver are all given.
You write load_config.
The five rules it must enforce
1. Unknown keys are rejected. Not ignored. A config loader that silently
drops tiemout: 30 is how a service runs at its default timeout for a year
while someone insists they configured it. Raise with kind="unknown".
2. Wrong types are rejected, and the message names the key path.
database.pool_size, not “invalid config”. An operator reading a crash log at
03:00 needs the path; anything less is a bisect through forty variables. Raise
with kind="type".
The trap: isinstance(True, int) is True, because bool subclasses int.
JSON true will land happily in your port: int field unless you check for
bool first. This is a real production bug, not a puzzle.
3. ${VAR} is resolved from the injected env — and only from it.
Not from os.environ. The parameter is what makes the function testable, and it
is what stops a test on your laptop from picking up your laptop’s environment.
A missing variable is an error (kind="env"), never a silent empty string.
4. Nesting is bounded, and the guard is iterative. CPython’s C JSON scanner
will happily parse 100,000 levels of nesting (measured on 3.14) — the stack
overflow arrives afterwards, in the recursive walk you write over the result — and a RecursionError is
not a ConfigError, so it escapes your except clause and becomes a 500. Note
the shape of the fix: you cannot write the depth guard recursively, because
it would blow the stack on exactly the input it exists to reject. Walk the
document with an explicit list used as a stack. MAX_DEPTH is 64; on
overflow raise kind="depth" with path "<root>" — depth is a property of the
document, not of one key.
5. The secret never escapes. Secret has a redacting __repr__, which
handles repr(config). The other half is on you: never put a value in an
exception message. Put the path and the type name. f"expected int, got str"
is a good message; f"expected int, got {value!r}" is a secret in your logs and
a credential rotation in your week. The tests string-search both repr(config)
and str(exc) for the live token.
Error shape
raise ConfigError(path, kind, detail)
# kind in {"unknown", "missing", "type", "env", "depth"}
Paths use dots for tables and brackets for lists: database.pool_size,
features[1], and a bare port at the top level. str(exc) must contain the
path — the driver asserts it.
The negative fixture
The module contains a function that is never executed:
def _negative_fixtures(raw: Mapping[str, object], env: Mapping[str, str]) -> None:
config = load_config(raw, env=env)
assert_type(config, Config)
config["debug"] # type: ignore[index]
--strict includes --warn-unused-ignores. So if you take the lazy route and
make Config a dict or a TypedDict, config["debug"] stops being an error,
the # type: ignore becomes unused, and mypy fails the submission. The
negative assertion is load-bearing: it is the machine-checked statement that you
returned a narrower type and not the same dict with a better name.
Leave that function exactly as it is. Leave solve exactly as it is.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.