• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1.. _unreachable:
2
3********************************************
4Unreachable Code and Exhaustiveness Checking
5********************************************
6
7Sometimes it is necessary to write code that should never execute, and
8sometimes we write code that we expect to execute, but that is actually
9unreachable. The type checker can help in both cases.
10
11In this guide, we'll cover:
12
13- ``Never``, the primitive type used for unreachable code
14- ``assert_never()``, a helper for exhaustiveness checking
15- Directly marking code as unreachable
16- Detecting unexpectedly unreachable code
17
18``Never`` and ``NoReturn``
19==========================
20
21Type theory has a concept of a
22`bottom type <https://en.wikipedia.org/wiki/Bottom_type>`__,
23a type that has no values. Concretely, this can be used to represent
24the return type of a function that never returns, or the argument type
25of a function that may never be called. You can also think of the
26bottom type as a union with no members.
27
28The Python type system has long provided a type called ``NoReturn``.
29While it was originally meant only for functions that never return,
30this concept is naturally extended to the bottom type in general, and all
31type checkers treat ``NoReturn`` as a general bottom type.
32
33To make the meaning of this type more explicit, Python 3.11 and
34typing-extensions 4.1 add a new primitive, ``Never``. To type checkers,
35it has the same meaning as ``NoReturn``.
36
37In this guide, we'll use ``Never`` for the bottom type, but if you cannot
38use it yet, you can always use ``typing.NoReturn`` instead.
39
40``assert_never()`` and Exhaustiveness Checking
41==============================================
42
43The ``Never`` type can be leveraged to perform static exhaustiveness checking,
44where we use the type checker to make sure that we covered all possible
45cases. For example, this can come up when code performs a separate action
46for each member of an enum, or for each type in a union.
47
48To have the type checker do exhaustiveness checking for us, we call a
49function with a parameter typed as ``Never``. The type checker will allow
50this call only if it can prove that the code is not reachable.
51
52As an example, consider this simple calculator:
53
54.. code:: python
55
56   import enum
57   from typing_extensions import Never
58
59   def assert_never(arg: Never) -> Never:
60       raise AssertionError("Expected code to be unreachable")
61
62   class Op(enum.Enum):
63       ADD = 1
64       SUBTRACT = 2
65
66   def calculate(left: int, op: Op, right: int) -> int:
67       match op:
68           case Op.ADD:
69               return left + right
70           case Op.SUBTRACT:
71               return left - right
72           case _:
73               assert_never(op)
74
75The ``match`` statement covers all members of the ``Op`` enum,
76so the ``assert_never()`` call is unreachable and the type checker
77will accept this code. However, if you add another member to the
78enum (say, ``MULTIPLY``) but don't update the ``match`` statement,
79the type checker will give an error saying that you are not handling
80the ``MULTIPLY`` case.
81
82Because the ``assert_never()`` helper function is frequently useful,
83it is provided by the standard library as ``typing.assert_never``
84starting in Python 3.11,
85and is also present in ``typing_extensions`` starting at version 4.1.
86However, it is also possible to define a similar function in your own
87code, for example if you want to customize the runtime error message.
88
89You can also use ``assert_never()`` with a sequence of ``if`` statements:
90
91.. code:: python
92
93   def calculate(left: int, op: Op, right: int) -> int:
94       if op is Op.ADD:
95           return left + right
96       elif op is Op.SUBTRACT:
97           return left - right
98       else:
99           assert_never(op)
100
101Marking Code as Unreachable
102=======================
103
104Sometimes a piece of code is unreachable, but the type system is not
105powerful enough to recognize that. For example, consider a function that
106finds the lowest unused street number in a street:
107
108.. code:: python
109
110   import itertools
111
112   def is_used(street: str, number: int) -> bool:
113       ...
114
115   def lowest_unused(street: str) -> int:
116       for i in itertools.count(1):
117           if not is_used(street, i):
118               return i
119       assert False, "unreachable"
120
121Because ``itertools.count()`` is an infinite iterator, this function
122will never reach the ``assert False`` statement. However, there is
123no way for the type checker to know that, so without the ``assert False``,
124the type checker will complain that the function is missing a return
125statement.
126
127Note how this is different from ``assert_never()``:
128
129- If we used ``assert_never()`` in the ``lowest_unused()`` function,
130  the type checker would produce an error, because the type checker
131  cannot prove that the line is unreachable.
132- If we used ``assert False`` instead of ``assert_never()`` in the
133  ``calculate()`` example above, we would not get the benefits of
134  exhaustiveness checking. If the code is actually reachable,
135  the type checker will not warn us and we could hit the assertion
136  at runtime.
137
138While ``assert False`` is the most idiomatic way to express this pattern,
139any statement that ends execution will do. For example, you could raise
140an exception or call a function that returns ``Never``.
141
142Detecting Unexpectedly Unreachable Code
143=======================================
144
145Another possible problem is code that is supposed to execute, but that
146can actually be statically determined to be unreachable.
147Some type checkers have an option that enables warnings for code
148detected as unreachable (e.g., ``--warn-unreachable`` in mypy).
149