blob: 16c37d8f4dc7177b05ed9b4afe75ebf084958b6b [file] [log] [blame]
.. _unreachable:
********************************************
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 code
- ``assert_never()``, a helper for exhaustiveness checking
- Directly marking code as unreachable
- Detecting unexpectedly unreachable code
``Never`` and ``NoReturn``
==========================
Type theory has a concept of a
`bottom type <https://en.wikipedia.org/wiki/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:
.. code:: python
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:
.. code:: python
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:
.. code:: python
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 the ``lowest_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 of ``assert_never()`` in the
``calculate()`` 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).