What do the bottom types `typing.Never` and `typing.NoReturn` express in an annotation?
answer
- a type with no values at all
- code that never comes back
- raise, exit, or loop forever
- not the same as returning None
- assert_never guards a closed dispatch
basics
~20 sNever is the bottom type — no value has it. As a return annotation, Never and NoReturn (equivalent since 3.11) say the function never returns normally: it raises, exits or loops forever, unlike returning None.
solid answer
~50 s`typing.Never` is the bottom type — the type with no values at all. Nothing can be passed where `Never` is required, so a parameter annotated `Never` marks a code path a checker must prove is unreachable. As a return annotation, `Never` — and `typing.NoReturn`, which has been interchangeable with it since 3.11 — says the function never returns normally: it raises, calls `sys.exit`, or loops forever. That differs from `-> None`, which says the function *does* return, with the value `None`. Checkers use this for reachability: statements after a call to a `NoReturn` function are dead code, and a branch ending in such a call needs no `return` of its own. The everyday use is exhaustiveness — in the fallback case of a `match` over a closed set, call `typing.assert_never(value)`, and adding a new variant turns that value's type into something other than `Never`, which the checker reports.
code
python · 14 linesfrom typing import NoReturn
def fail(message: str) -> NoReturn:
raise RuntimeError(message)
def clamp(value: int) -> int:
if value < 0:
fail("negative input") # a checker knows control stops here
return value
print(clamp(4))go deeper
Recall that -> None means the function returns and gives back None, while -> NoReturn means it never comes back at all because it raises or exits. Knowing that one distinction is enough at this level.
Explain the mechanics: the bottom type has no values, so nothing can be passed for a Never parameter, and a NoReturn call makes following statements unreachable. Be ready to name typing.assert_never and the 3.11 arrival of Never.
Show where these earn their keep in a real codebase: shared raise-helpers that preserve narrowing, dispatches over closed sets guarded by assert_never, and the discipline of not annotating NoReturn on something that can in fact return.
Own the tradeoff of leaning on static exhaustiveness: it works only where the variant set is genuinely closed and everyone runs the checker, so decide which domain unions are worth freezing and where runtime validation must carry the guarantee instead.
A type is, in effect, a set of values. `object` is the set of *all* values, which is why it sits at the top. The **bottom type** is the other extreme: the empty set, a type with no members whatsoever. Python spells it `typing.Never`, added in 3.11, and `typing.NoReturn`, which arrived earlier (3.6.2) for the return-position use and has been documented as interchangeable with `Never` since 3.11. ## What "no values" buys you Because no value has type `Never`, two useful consequences fall out. **In a parameter position**, `Never` means the function can never legally be called — there is no argument you could supply. That sounds useless until you realise it is exactly how you ask a checker to *prove* a branch is unreachable. If the checker can only reach that call with a value it has narrowed to `Never`, all is well; if some value can still arrive there, the call is an error and the checker tells you. **In a return position**, `Never`/`NoReturn` means the function never returns normally. It either raises, terminates the process, or loops forever. This is not a claim about the *value* returned, it is a claim that control never comes back: ```python def fail(message: str) -> NoReturn: raise RuntimeError(message) ``` ## The distinction people get wrong: `-> None` `-> None` and `-> NoReturn` are frequently confused, and they are opposites in an important sense. `-> None` says the function *does* return, and the value it hands back is the singleton `None` — an ordinary type (`types.NoneType`) with exactly one member. `-> NoReturn` says control never reaches the caller at all. A function whose body ends with a bare `return` or falls off the end has `-> None`; only a function that unconditionally raises, exits or blocks forever has `-> NoReturn`. ## Reachability and control flow The payoff is flow analysis. When a checker sees a call to a function annotated `NoReturn`, it marks everything after that call in the same block as unreachable. That has two visible effects in real code. First, a helper like `fail(...)` can end an `if` branch, and the checker will not complain that the branch is missing a `return` — it knows control cannot continue. Second, narrowing survives: after `if x is None: fail("missing")`, the checker treats `x` as non-`None` for the rest of the function, exactly as if you had written `raise` inline. That is what makes small `NoReturn` helpers worth annotating rather than leaving to inference. `sys.exit` is the canonical standard-library example: it raises `SystemExit`, so it never returns to its caller and is annotated accordingly. ## Exhaustiveness with `assert_never` The most valuable everyday use is proving a dispatch covers every case. Write the dispatch over a closed set — an enum, a union of literal values, a set of subclasses — and in the fallback branch call `typing.assert_never(value)`, added in 3.11. In the good case the checker has narrowed away every alternative, so the value's type in that branch is `Never`, and the call is accepted. Add a new variant later and forget to handle it, and the residual type is that new variant rather than `Never`, so the call is now a type error pointing straight at the dispatch you forgot to update. It converts "I hope I updated every `match`" into a check that fails at review time. At runtime `assert_never` is a real function: if control actually reaches it, it raises `AssertionError` with the offending value in the message. So it is a static guarantee with a runtime backstop, not a no-op. ## Runtime reality None of this is enforced by the interpreter. A function annotated `-> NoReturn` that quietly returns a value will run perfectly happily; only a checker objects. Likewise `Never` has no runtime power — you can bind a name to anything regardless of what its annotation says. These annotations are claims for the checker's flow analysis, and their value is entirely in that analysis catching the paths you did not think about. ## When to reach for them Use `NoReturn` on process-terminating helpers, on error-raising helpers used from several branches, and on retry loops that never fall through. Use `Never` with `assert_never` at the end of every dispatch over a closed set. Do not use either as a general-purpose "this should not happen" marker on code that in fact can happen — the annotation is a claim, and if it is false the checker's downstream reasoning about reachability becomes wrong too.
- How does annotating a small error helper `-> NoReturn` change what a checker infers at its call sites?It makes the call a control-flow terminator. Everything after it in that block becomes unreachable, so a branch ending in the helper needs no `return` of its own, and any narrowing that led into the branch survives afterwards — after `if x is None: fail("missing")`, `x` is treated as non-`None` below. Without the annotation the checker assumes the helper returns and starts demanding returns and `None` handling you do not need.
- What actually happens at runtime if a function annotated `-> NoReturn` does return?Nothing at all — it returns normally. The interpreter never enforces annotations, so the claim is only checked statically. The cost is subtler than a crash: the checker has already treated the code after every call site as unreachable, so real bugs in that dead-looking code go unexamined, and narrowing that depended on the call is now wrong.
- Why does `typing.assert_never` catch a missing enum case at review time rather than in production?Because in the fallback branch the checker has narrowed the value by eliminating every case you handled. If you handled them all, the remaining type is `Never` and the call type-checks. Add a variant and forget the branch, and the remaining type is that new variant, which is not assignable to `Never`, so the checker reports an error at the `assert_never` call. At runtime it would only raise `AssertionError` if that path were actually exercised.
Never is a room with no door: you cannot put anything inside it, so if the checker can show control reaching that room, something upstream is wrong.
saying these in an interview costs you the question
- Says `-> NoReturn` is just another way to write `-> None`
- Thinks the interpreter enforces a NoReturn annotation
- Believes some value can have type Never
- Cannot connect Never to unreachable-code proofs
- Claims Never was available before Python 3.11
- Uses NoReturn on a function that sometimes returns