We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modern Syntax and Modernisation step 14 of 18
Guards, OR patterns, AS patterns and the binding rules
Guards are what stop a match degenerating into “destructure here, test the
field twenty lines later”. The condition and the destructuring live in the same
line, so it is not possible to test a field you did not extract from this
shape.
Three constructs, three rules
Guards. case Http(status=s) if s >= 500: — the pattern binds first, then
the guard runs with the bindings visible. If the guard is false, control
falls through to the next case; it does not leave the match. This is the
single most useful thing to know about guards, and the thing most people get
wrong when reading unfamiliar code.
Because a guarded pattern is refutable, case _ if debug: may legally appear
before other cases — the “irrefutable pattern must be last” rule does not apply
to it.
OR patterns. case 404 | 410:. Alternatives are tried left to right, and
the first match wins. The hard constraint: every alternative must bind the
same names. case [x] | [x, y]: is a SyntaxError, not a runtime surprise.
That is the language enforcing statically the property that mypy’s
possibly-undefined error code exists to catch everywhere else — a rare case
of the grammar doing the type checker’s job.
AS patterns. pattern as name binds the subject of a subpattern:
case Http(status=(404 | 410) as status, path=path):
captures whichever of 404/410 matched, so the handler can report it. Without the AS you would have to duplicate the case or re-read the attribute.
What you are building
An event router.
def parse(raw: Mapping[str, object]) -> Event # mapping patterns
def route(event: Event) -> str # class patterns + guards
def solve(events: list[dict[str, object]]) -> list[str]
parse turns a raw mapping into Http(status, path), Timer(seconds) or
User(name, age), raising ValueError for anything else. solve catches that
and records "invalid".
route, in order:
| case | result |
|---|---|
Http with status >= 500 |
"alert:{path}" |
Http with status 404 or 410 |
"gone:{status}:{path}" |
Http whose path starts /admin |
"forbidden" |
any other Http |
"ok:{path}" |
Timer with seconds > 60 |
"slow-timer:{seconds}" |
any other Timer |
"timer" |
User aged 18 or over |
"adult:{name}" |
any other User |
"minor:{name}" |
The tests deliberately include a 200 on /admin/panel: the first guard fails,
the OR pattern does not match, and the third case’s guard succeeds. If you read
“a failing guard exits the match” you will produce "ok:/admin/panel" and
leave an authorisation hole.
The typing payoff
parse takes Mapping[str, object] — the honest type of decoded JSON — and
returns a real union. The mapping pattern
case {"kind": "http", "status": int() as status, "path": str() as path}:
does the narrowing and the validation and the extraction in one expression,
with no cast, no assert isinstance, and no # type: ignore. That is the
best argument for match in a strictly-typed codebase: it is the only
construct that narrows several fields of an untyped mapping at once.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.