Python Never: Mark Unreachable Code

Published on: August 29, 2026
Reading time: 5 minutes
Close view of a python resting on sandy terrain outdoors in natural habitat.

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 += 1

The 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 None

The 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Detailed view of programming code in a dark theme on a computer screen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python NewType: Keep IDs Distinct

    Learn Python NewType to keep IDs, codes, units, and primitive values distinct, validate boundaries, and prevent domain mix-ups.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Concatenate: Change Parameters

    Learn Python Concatenate to add or hide leading parameters in typed decorators with ParamSpec, contexts, locks, and dependencies.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A close-up view of a person's hand signing a business contract on a desk with a pen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ParamSpec: Preserve Signatures

    Learn Python ParamSpec to preserve complete function signatures in decorators, callbacks, async wrappers, and higher-order utilities.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypeIs: Narrow Both Branches

    Learn Python TypeIs to narrow true and false branches, compare it with TypeGuard, and build sound reusable type predicates.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Hands typing code on a laptop in a workspace. Indoor setting focused on software development.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypeGuard: Refine Types Safely

    Learn Python TypeGuard to narrow types and validate collections, TypedDict, Protocol, and external data with runtime checks and static safety.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python typing.Self: Fluent Return Types

    Learn Python typing.Self for fluent methods, classmethods, builders, clones, Protocol, context managers, generics, and subclass-preserving returns.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026