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