We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modern Syntax and Modernisation step 13 of 18
Exhaustive matching with assert_never
This is the highest-leverage typing technique in the whole course. Done once, per union, it converts “somebody added a variant and forgot to handle it” from a production incident into a CI failure.
The mechanism
from typing import assert_never
def describe(event: Event) -> str:
match event:
case Created(entity):
return f"created {entity}"
case Updated(entity, field):
return f"updated {entity}.{field}"
case _ as unhandled:
assert_never(unhandled)
assert_never is declared def assert_never(arg: Never, /) -> Never. If the
checker can prove every variant is handled, the subject’s type at the final
case is Never and the call type-checks. Add a third variant to Event and
the subject is Deleted, which is not Never — a hard error, in this file,
pointing at this line, in every checker that implements the typing spec.
Two things it is not:
-
It is not a runtime check you rely on. It raises
AssertionErrorwhen reached, which is a backstop for code that got past the checker viaAny, acast, or an untyped caller. -
It is not free. It requires a closed subject type — an
Enum, aLiteral[...], or a tagged union of dataclasses. Exhaustiveness overintorstris not a thing.
The alternative is mypy’s opt-in exhaustive-match error code, which reports a
non-exhaustive match with no code change at all. It is mypy-only; the
assert_never idiom works everywhere and documents the intent in the source.
Use both.
What defeats it
case _:
return "unknown" # exhaustiveness: gone
A wildcard that returns makes the match total, so the checker has nothing to
complain about — forever. Same for a trailing else. And remember the previous
item’s trap: case PENDING: is a capture, so it matches everything, which also
makes the match total and every later case dead.
What you are building
type Event = Created | Updated | Deleted | Archived
Four frozen dataclasses (given), plus a Level StrEnum with four members.
-
severity(level: Level) -> int— 10/20/30/40, exhaustively, ending inassert_never. -
describe(event: Event) -> str—"created {entity}","updated {entity}.{field}","deleted {entity}","archived {entity} ({reason})", ending inassert_never. -
parse(raw) -> Event— mapping patterns;ValueErroron anything else. -
probe_backstop() -> str— assigndescribeto aCallable[..., str], call it with aRogueinstance (a fifth dataclass that is deliberately not in the union), and return"assert_never"if that raisesAssertionError. -
solve(events, probe_unknown)— for each event,"{severity}|{describe}", or"invalid"if it does not parse or its"level"is not aLevel. The default level is"info". Whenprobe_unknownis true, append the probe result.
The probe is not a stunt. Callable[..., R] erasing the parameter list is a
real hole in every codebase that stores handlers in a registry, and the
AssertionError is what tells you which one leaked through — instead of a
plausible-looking "unknown" string travelling downstream.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.