This is a short item. NoReturn and Never are owned by the type-system track
— 1.3 (the bottom type and where it comes from) and 1.4 (exhaustiveness
checking with assert_never). Go there for the theory, the variance story, and
the match exhaustiveness pattern.
What belongs here, in a track about failure, is one idiom and one consequence.
The idiom
A helper whose entire job is to raise:
from typing import NoReturn
def fail(message: str) -> NoReturn:
raise ValidationError(message)
Annotate it -> NoReturn and every caller’s control-flow analysis gets it
right:
def parse_port(raw: str) -> int:
if not raw.isdigit():
fail(f"port must be numeric, got {raw!r}")
return int(raw) # mypy knows this line is only reached when raw is numeric
Annotate it -> None instead and mypy believes fail() can return, so it
believes parse_port can fall through, and now you are fighting the checker
about a branch that cannot happen. The annotation is not documentation; it is
the fact that makes narrowing work.
The consequence
The same reasoning applies to any method that cannot return. In this track’s
Result problem, Err.unwrap() is annotated -> Never:
def unwrap(self) -> Never:
raise UnwrapError(...)
On the union Ok[str, E] | Err[str, E], mypy joins str with Never and gets
str — because Never is the bottom type and contributes nothing to a union.
The happy path keeps its exact type even though one arm cannot return.
Annotate it -> None and the join becomes str | None. Every caller now has to
handle a None that can never occur, and your carefully typed Result has made
the code worse than the exceptions it replaced. One wrong annotation, and the
abstraction stops paying for itself.
NoReturn and Never are spellings of the same type; Never (3.11+) is
preferred in new code, NoReturn remains for return annotations and reads
better there.
→ See 1.3 and 1.4 for the full treatment.