Skip to content

Commit 46094aa

Browse files
Add guide to unreachable code (#1065)
1 parent 77e7937 commit 46094aa

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

docs/source/guides.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Type System Guides
77
:caption: Contents:
88

99
libraries
10+
unreachable

docs/source/unreachable.rst

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
.. _unreachable:
2+
3+
********************************************
4+
Unreachable Code and Exhaustiveness Checking
5+
********************************************
6+
7+
Sometimes it is necessary to write code that should never execute, and
8+
sometimes we write code that we expect to execute, but that is actually
9+
unreachable. The type checker can help in both cases.
10+
11+
In 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+
21+
Type theory has a concept of a
22+
`bottom type <https://en.wikipedia.org/wiki/Bottom_type>`__,
23+
a type that has no values. Concretely, this can be used to represent
24+
the return type of a function that never returns, or the argument type
25+
of a function that may never be called. You can also think of the
26+
bottom type as a union with no members.
27+
28+
The Python type system has long provided a type called ``NoReturn``.
29+
While it was originally meant only for functions that never return,
30+
this concept is naturally extended to the bottom type in general, and all
31+
type checkers treat ``NoReturn`` as a general bottom type.
32+
33+
To make the meaning of this type more explicit, Python 3.11 and
34+
typing-extensions 4.1 add a new primitive, ``Never``. To type checkers,
35+
it has the same meaning as ``NoReturn``.
36+
37+
In this guide, we'll use ``Never`` for the bottom type, but if you cannot
38+
use it yet, you can always use ``typing.NoReturn`` instead.
39+
40+
``assert_never()`` and Exhaustiveness Checking
41+
==============================================
42+
43+
The ``Never`` type can be leveraged to perform static exhaustiveness checking,
44+
where we use the type checker to make sure that we covered all possible
45+
cases. For example, this can come up when code performs a separate action
46+
for each member of an enum, or for each type in a union.
47+
48+
To have the type checker do exhaustiveness checking for us, we call a
49+
function with a parameter typed as ``Never``. The type checker will allow
50+
this call only if it can prove that the code is not reachable.
51+
52+
As 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+
75+
The ``match`` statement covers all members of the ``Op`` enum,
76+
so the ``assert_never()`` call is unreachable and the type checker
77+
will accept this code. However, if you add another member to the
78+
enum (say, ``MULTIPLY``) but don't update the ``match`` statement,
79+
the type checker will give an error saying that you are not handling
80+
the ``MULTIPLY`` case.
81+
82+
Because the ``assert_never()`` helper function is frequently useful,
83+
it is provided by the standard library as ``typing.assert_never``
84+
starting in Python 3.11,
85+
and is also present in ``typing_extensions`` starting at version 4.1.
86+
However, it is also possible to define a similar function in your own
87+
code, for example if you want to customize the runtime error message.
88+
89+
You 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+
101+
Marking Code as Unreachable
102+
=======================
103+
104+
Sometimes a piece of code is unreachable, but the type system is not
105+
powerful enough to recognize that. For example, consider a function that
106+
finds 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+
121+
Because ``itertools.count()`` is an infinite iterator, this function
122+
will never reach the ``assert False`` statement. However, there is
123+
no way for the type checker to know that, so without the ``assert False``,
124+
the type checker will complain that the function is missing a return
125+
statement.
126+
127+
Note 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+
138+
While ``assert False`` is the most idiomatic way to express this pattern,
139+
any statement that ends execution will do. For example, you could raise
140+
an exception or call a function that returns ``Never``.
141+
142+
Detecting Unexpectedly Unreachable Code
143+
=======================================
144+
145+
Another possible problem is code that is supposed to execute, but that
146+
can actually be statically determined to be unreachable.
147+
Some type checkers have an option that enables warnings for code
148+
detected as unreachable (e.g., ``--warn-unreachable`` in mypy).

0 commit comments

Comments
 (0)