We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Precise Type Hints with Literals and Generics step 5 of 26
Make mypy Report an Unhandled Event with assert_never
An audit log receives events as strings, and you turn each one into a
sentence. Next month a teammate adds "archived" to the Event type and
forgets to write its sentence. You want mypy to find the missing case before
a user does.
assert solve(["created", "archived", "deleted"], "order-7") == [
"order-7 was created.",
"unknown event: archived",
"order-7 was deleted permanently.",
]
The starter is missing a case
The starter's describe handles "created" and "updated", then ends with
case _: assert_never(event). mypy reports three errors for the starter,
and the one in describe reads:
Argument 1 to "assert_never" has incompatible type "Literal['deleted']"; expected "Never" [arg-type]
Never is the type with no values. Each case removes one value from the
type of event for the cases below it. When every value has its own case, nothing
is left, so at case _ the type of event is Never. assert_never
accepts only Never, so the call passes mypy. Here "deleted" has no case
yet, so mypy names it.
A match that handles every possible value is called exhaustive. With
assert_never at the end, mypy reports the same error whenever someone adds
a value to Event and forgets its case. If a value does reach
assert_never at runtime, it raises AssertionError.
What to write
The starter defines the type and a tuple of the valid values:
type Event = Literal["created", "updated", "deleted"]
EVENTS: Final[tuple[Event, ...]] = ("created", "updated", "deleted")
parse_event(raw: str) -> Eventreturnsrawas anEventwhen it is exactly one of the strings inEVENTS. Otherwise it raisesValueError(f"unknown event: {raw}").- A
Literaltype checks nothing at runtime, soparse_eventis where you check the input. Loop overEVENTSand return the item that equalsraw. Its type is alreadyEvent, so you need nocast. describe(event: Event, name: str) -> strusesmatchto return these sentences, withnameinserted unchanged:
| Event | Result |
|---|---|
created |
"<name> was created." |
updated |
"<name> was updated." |
deleted |
"<name> was deleted permanently." |
- Keep
case _: assert_never(event)as the last case. Acase _that returns a default string also passes mypy, but it hides a missing case instead of reporting it. solve(events: list[str], name: str) -> list[str]parses and describes each event in order. Whenparse_eventraisesValueError, append the error message and go on to the next event. Keep duplicates.
Edge cases
- Matching is exact. Do not trim spaces or change case, so
"Created"," created"and""are all unknown. - For
"", the message is"unknown event: ", which ends with the space after the colon. - An empty
eventslist returns[].
Try it
Once your code passes, delete the case "deleted" branch and run mypy again.
You get the error above. Put the branch back before you submit.
The diagram follows event through three comparisons. Choose else at
each step, and no event is left. That empty type is the Never that
assert_never expects. Choose then to follow one event into its branch.
Loading visualization…
Going further
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.