Some functions never return normally: they always raise an exception, terminate the process, or remain in a non-ending control flow. Other branches should be impossible after every member of a union has been handled. typing.Never represents the empty type, with no possible values, and allows static checkers to understand these situations.
This guide explains functions that do not return, assert_never() for exhaustiveness, Literal, Enum, match/case, callbacks, the relationship with NoReturn, and common mistakes.
The type with no values
Regular types describe sets of values. int contains integers, str contains strings, and int | str contains both. Never describes an empty set: there is no normal Python value that inhabits the type.
Never is therefore known as a bottom type. It can be treated as a subtype of every type because the empty set is contained in every set.
A function that always raises
from typing import Never
def fail(message: str) -> Never:
raise RuntimeError(message)The annotation says that control flow never continues after the call.
user = find_user()
if user is None:
fail("user not found")
print(user.name)After the branch, the checker knows that user cannot still be None.
Terminating the process
import sys
from typing import Never
def exit_with_error(code: int, message: str) -> Never:
print(message, file=sys.stderr)
raise SystemExit(code)SystemExit is an exception, so the function has no normal return path. A helper that always calls another Never-returning function also satisfies the contract.
Never versus None
None is a real value. A function annotated with -> None returns normally even though it produces no useful result.
def log(text: str) -> None:
print(text)A -> Never function does not reach a normal return. Mixing the two contracts is incorrect.
Never versus NoReturn
typing.NoReturn was introduced specifically for functions that never return. Never generalizes the empty-type concept and can appear in additional positions. For function returns, both communicate essentially the same behavior to modern checkers.
from typing import NoReturn
def abort() -> NoReturn:
raise SystemExit(1)New code may prefer Never, while libraries supporting older Python versions may keep NoReturn or import Never from typing_extensions.
Exhaustiveness with assert_never
from typing import Literal, assert_never
State = Literal["new", "paid", "shipped"]
def label(state: State) -> str:
match state:
case "new":
return "New"
case "paid":
return "Paid"
case "shipped":
return "Shipped"
case _:
assert_never(state)When every State value has been handled, the checker sees state as Never in the default case. If a new Literal is added without a matching branch, the call to assert_never() becomes a type error.
Why not only assert False
case _:
assert False, "impossible state"This fails at runtime but may not force the checker to prove that the case is unreachable. assert_never() combines a static exhaustiveness check with a runtime failure if the invariant is broken.
Exhaustiveness with if and elif
def color(state: State) -> str:
if state == "new":
return "gray"
elif state == "paid":
return "blue"
elif state == "shipped":
return "green"
else:
assert_never(state)The pattern does not require match/case. What matters is that the checker progressively narrows the union.
Never with Enum
from enum import Enum, auto
from typing import assert_never
class Role(Enum):
ADMIN = auto()
EDITOR = auto()
READER = auto()
def permissions(role: Role) -> set[str]:
match role:
case Role.ADMIN:
return {"read", "edit", "delete"}
case Role.EDITOR:
return {"read", "edit"}
case Role.READER:
return {"read"}
case _:
assert_never(role)Adding a new enum member can make the function fail static exhaustiveness checks until the new case is handled.
Unions of classes
from dataclasses import dataclass
@dataclass
class Text:
value: str
@dataclass
class Number:
value: float
Node = Text | Number
def render(node: Node) -> str:
if isinstance(node, Text):
return node.value
if isinstance(node, Number):
return str(node.value)
assert_never(node)The final line should remain impossible while Node contains only the two classes.
Never as a parameter
A parameter annotated as Never means that the function cannot be called with a normal value.
def impossible(value: Never) -> str:
return "should not execute"This pattern appears in exhaustiveness helpers and advanced generic APIs. It is uncommon in ordinary application code.
Never in generic inference
Type inference may produce Never when no alternative is possible or a value is known to be empty without useful element information. Exact behavior differs between checkers. Public APIs should use explicit annotations where empty inference might confuse callers.
Callbacks that never return
from collections.abc import Callable
def run_or_abort(
operation: Callable[[], int],
abort: Callable[[Exception], Never],
) -> int:
try:
return operation()
except Exception as error:
abort(error)Because the abort callback never returns, the outer function needs no additional return statement in the exception branch.
Infinite loops
def server() -> Never:
while True:
handle_next_request()A function with a provably infinite loop may be annotated as Never. If a break, conditional return, or other terminating path exists, the annotation may be false.
Generators are not Never functions
A generator can yield forever, but calling the generator function immediately returns a generator object. The function should not be annotated with -> Never.
from collections.abc import Iterator
def count_forever() -> Iterator[int]:
number = 0
while True:
yield number
number += 1The iterator is infinite, but creating it returns normally.
A function that sometimes raises
def load(path: str) -> bytes:
if not exists(path):
raise FileNotFoundError(path)
return read(path)This function returns bytes on some paths, so its return type is bytes, not bytes | Never. Never adds no values to a union and is absorbed by other members.
Never with overloads
from typing import Literal, overload
@overload
def convert(value: str, *, strict: Literal[True]) -> int: ...
@overload
def convert(value: str, *, strict: Literal[False]) -> int | None: ...
def convert(value: str, *, strict: bool) -> int | None:
try:
return int(value)
except ValueError:
if strict:
fail("invalid integer")
return NoneThe Never helper lets the checker understand that the strict branch cannot continue with None.
Exhaustiveness as APIs evolve
The strongest benefit of assert_never() appears when types evolve. Adding a member to a union, Enum, or Literal makes exhaustive functions fail in continuous integration until the new case is handled. This prevents silent default branches from hiding missing business rules.
When a fallback is appropriate
External and untrusted data may contain unknown values even if the annotation says otherwise. A validation error, log entry, or fallback may be more appropriate than assert_never at that boundary. Use static exhaustiveness after values have been validated and are controlled by the program.
Runtime behavior of assert_never
If called, assert_never() raises an exception because it received a value that should have been impossible. This is a useful defense, but its primary purpose is making the checker verify the argument type.
Version compatibility
Never and assert_never are available in modern versions of typing. Older supported versions can use typing_extensions.Never and typing_extensions.assert_never.
Common mistakes
- Annotating a None-returning function as Never: the contracts differ.
- Using Never for an infinite generator: calling the function returns an iterator.
- Hiding a real fallback: external data may have unknown variants.
- Calling assert_never before full narrowing: the checker correctly reports remaining cases.
- Implementing a function that can return: the annotation becomes false.
- Confusing NoReturn with no useful value: it means no normal return.
Complete command example
from dataclasses import dataclass
from typing import assert_never
@dataclass
class Create:
name: str
@dataclass
class Rename:
id: int
name: str
@dataclass
class Delete:
id: int
Command = Create | Rename | Delete
def execute(command: Command) -> None:
match command:
case Create(name=name):
create(name)
case Rename(id=id_value, name=name):
rename(id_value, name)
case Delete(id=id_value):
delete(id_value)
case _:
assert_never(command)Adding a new class to Command requires updating the dispatcher. The failure appears during static analysis before the new variant reaches production.
Best practices
Use Never for small helpers that genuinely terminate control flow. Use assert_never only after complete narrowing, not as a substitute for boundary validation. Run a checker in CI and treat exhaustiveness failures as required business-rule updates.
Conclusion
typing.Never represents the total absence of values and describes non-returning functions, abort callbacks, and unreachable paths. With assert_never(), unions and enums become exhaustive contracts that evolve safely.
The official Python Never documentation and assert_never documentation define the behavior. Use Never only when no normal return path exists, and keep real fallbacks for open or unvalidated data.







