Unreachable Code and Exhaustiveness Checking¶
Sometimes it is necessary to write code that should never execute, and sometimes we write code that we expect to execute, but that is actually unreachable. The type checker can help in both cases.
In this guide, we’ll cover:
Never
, the primitive type used for unreachable codeassert_never()
, a helper for exhaustiveness checkingDirectly marking code as unreachable
Detecting unexpectedly unreachable code
Never
and NoReturn
¶
Type theory has a concept of a bottom type, a type that has no values. Concretely, this can be used to represent the return type of a function that never returns, or the argument type of a function that may never be called. You can also think of the bottom type as a union with no members.
The Python type system has long provided a type called NoReturn
.
While it was originally meant only for functions that never return,
this concept is naturally extended to the bottom type in general, and all
type checkers treat NoReturn
as a general bottom type.
To make the meaning of this type more explicit, Python 3.11 and
typing-extensions 4.1 add a new primitive, Never
. To type checkers,
it has the same meaning as NoReturn
.
In this guide, we’ll use Never
for the bottom type, but if you cannot
use it yet, you can always use typing.NoReturn
instead.
assert_never()
and Exhaustiveness Checking¶
The Never
type can be leveraged to perform static exhaustiveness checking,
where we use the type checker to make sure that we covered all possible
cases. For example, this can come up when code performs a separate action
for each member of an enum, or for each type in a union.
To have the type checker do exhaustiveness checking for us, we call a
function with a parameter typed as Never
. The type checker will allow
this call only if it can prove that the code is not reachable.
As an example, consider this simple calculator:
import enum
from typing_extensions import Never
def assert_never(arg: Never) -> Never:
raise AssertionError("Expected code to be unreachable")
class Op(enum.Enum):
ADD = 1
SUBTRACT = 2
def calculate(left: int, op: Op, right: int) -> int:
match op:
case Op.ADD:
return left + right
case Op.SUBTRACT:
return left - right
case _:
assert_never(op)
The match
statement covers all members of the Op
enum,
so the assert_never()
call is unreachable and the type checker
will accept this code. However, if you add another member to the
enum (say, MULTIPLY
) but don’t update the match
statement,
the type checker will give an error saying that you are not handling
the MULTIPLY
case.
Because the assert_never()
helper function is frequently useful,
it is provided by the standard library as typing.assert_never
starting in Python 3.11,
and is also present in typing_extensions
starting at version 4.1.
However, it is also possible to define a similar function in your own
code, for example if you want to customize the runtime error message.
You can also use assert_never()
with a sequence of if
statements:
def calculate(left: int, op: Op, right: int) -> int:
if op is Op.ADD:
return left + right
elif op is Op.SUBTRACT:
return left - right
else:
assert_never(op)
Marking Code as Unreachable¶
Sometimes a piece of code is unreachable, but the type system is not powerful enough to recognize that. For example, consider a function that finds the lowest unused street number in a street:
import itertools
def is_used(street: str, number: int) -> bool:
...
def lowest_unused(street: str) -> int:
for i in itertools.count(1):
if not is_used(street, i):
return i
assert False, "unreachable"
Because itertools.count()
is an infinite iterator, this function
will never reach the assert False
statement. However, there is
no way for the type checker to know that, so without the assert False
,
the type checker will complain that the function is missing a return
statement.
Note how this is different from assert_never()
:
If we used
assert_never()
in thelowest_unused()
function, the type checker would produce an error, because the type checker cannot prove that the line is unreachable.If we used
assert False
instead ofassert_never()
in thecalculate()
example above, we would not get the benefits of exhaustiveness checking. If the code is actually reachable, the type checker will not warn us and we could hit the assertion at runtime.
While assert False
is the most idiomatic way to express this pattern,
any statement that ends execution will do. For example, you could raise
an exception or call a function that returns Never
.
Detecting Unexpectedly Unreachable Code¶
Another possible problem is code that is supposed to execute, but that
can actually be statically determined to be unreachable.
Some type checkers have an option that enables warnings for code
detected as unreachable (e.g., --warn-unreachable
in mypy).