[Python-checkins] bpo-46475: Add typing.Never and typing.assert_never (GH-30842)

gvanrossum webhook-mailer at python.org
Tue Feb 8 13:50:33 EST 2022


https://github.com/python/cpython/commit/243436f3779c1e7bed1fd4b23d5a8ec5eff40699
commit: 243436f3779c1e7bed1fd4b23d5a8ec5eff40699
branch: main
author: Jelle Zijlstra <jelle.zijlstra at gmail.com>
committer: gvanrossum <gvanrossum at gmail.com>
date: 2022-02-08T10:50:26-08:00
summary:

bpo-46475: Add typing.Never and typing.assert_never (GH-30842)

files:
A Misc/NEWS.d/next/Library/2022-01-23-15-35-07.bpo-46475.UCe18S.rst
M Doc/library/typing.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 8c1c34e90507f..94a46b01a1a8c 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -574,6 +574,34 @@ These can be used as types in annotations and do not support ``[]``.
    * Every type is compatible with :data:`Any`.
    * :data:`Any` is compatible with every type.
 
+.. data:: Never
+
+   The `bottom type <https://en.wikipedia.org/wiki/Bottom_type>`_,
+   a type that has no members.
+
+   This can be used to define a function that should never be
+   called, or a function that never returns::
+
+     from typing import Never
+
+     def never_call_me(arg: Never) -> None:
+         pass
+
+     def int_or_str(arg: int | str) -> None:
+         never_call_me(arg)  # type checker error
+         match arg:
+             case int():
+                 print("It's an int")
+             case str():
+                 print("It's a str")
+             case _:
+                 never_call_me(arg)  # ok, arg is of type Never
+
+   .. versionadded:: 3.11
+
+      On older Python versions, :data:`NoReturn` may be used to express the
+      same concept. ``Never`` was added to make the intended meaning more explicit.
+
 .. data:: NoReturn
 
    Special type indicating that a function never returns.
@@ -584,6 +612,12 @@ These can be used as types in annotations and do not support ``[]``.
       def stop() -> NoReturn:
           raise RuntimeError('no way')
 
+   ``NoReturn`` can also be used as a
+   `bottom type <https://en.wikipedia.org/wiki/Bottom_type>`_, a type that
+   has no values. Starting in Python 3.11, the :data:`Never` type should
+   be used for this concept instead. Type checkers should treat the two
+   equivalently.
+
    .. versionadded:: 3.5.4
    .. versionadded:: 3.6.2
 
@@ -1979,6 +2013,28 @@ Functions and decorators
    runtime we intentionally don't check anything (we want this
    to be as fast as possible).
 
+.. function:: assert_never(arg, /)
+
+   Assert to the type checker that a line of code is unreachable.
+
+   Example::
+
+       def int_or_str(arg: int | str) -> None:
+           match arg:
+               case int():
+                   print("It's an int")
+               case str():
+                   print("It's a str")
+               case _ as unreachable:
+                   assert_never(unreachable)
+
+   If a type checker finds that a call to ``assert_never()`` is
+   reachable, it will emit an error.
+
+   At runtime, this throws an exception when called.
+
+   .. versionadded:: 3.11
+
 .. function:: reveal_type(obj)
 
    Reveal the inferred static type of an expression.
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 4d1c50ce73549..78e58928332f0 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -9,7 +9,7 @@
 from unittest import TestCase, main, skipUnless, skip
 from copy import copy, deepcopy
 
-from typing import Any, NoReturn
+from typing import Any, NoReturn, Never, assert_never
 from typing import TypeVar, AnyStr
 from typing import T, KT, VT  # Not in __all__.
 from typing import Union, Optional, Literal
@@ -124,38 +124,56 @@ def test_any_works_with_alias(self):
         typing.IO[Any]
 
 
-class NoReturnTests(BaseTestCase):
+class BottomTypeTestsMixin:
+    bottom_type: ClassVar[Any]
 
-    def test_noreturn_instance_type_error(self):
+    def test_instance_type_error(self):
         with self.assertRaises(TypeError):
-            isinstance(42, NoReturn)
+            isinstance(42, self.bottom_type)
 
-    def test_noreturn_subclass_type_error(self):
+    def test_subclass_type_error(self):
         with self.assertRaises(TypeError):
-            issubclass(Employee, NoReturn)
+            issubclass(Employee, self.bottom_type)
         with self.assertRaises(TypeError):
-            issubclass(NoReturn, Employee)
-
-    def test_repr(self):
-        self.assertEqual(repr(NoReturn), 'typing.NoReturn')
+            issubclass(NoReturn, self.bottom_type)
 
     def test_not_generic(self):
         with self.assertRaises(TypeError):
-            NoReturn[int]
+            self.bottom_type[int]
 
     def test_cannot_subclass(self):
         with self.assertRaises(TypeError):
-            class A(NoReturn):
+            class A(self.bottom_type):
                 pass
         with self.assertRaises(TypeError):
-            class A(type(NoReturn)):
+            class A(type(self.bottom_type)):
                 pass
 
     def test_cannot_instantiate(self):
         with self.assertRaises(TypeError):
-            NoReturn()
+            self.bottom_type()
         with self.assertRaises(TypeError):
-            type(NoReturn)()
+            type(self.bottom_type)()
+
+
+class NoReturnTests(BottomTypeTestsMixin, BaseTestCase):
+    bottom_type = NoReturn
+
+    def test_repr(self):
+        self.assertEqual(repr(NoReturn), 'typing.NoReturn')
+
+
+class NeverTests(BottomTypeTestsMixin, BaseTestCase):
+    bottom_type = Never
+
+    def test_repr(self):
+        self.assertEqual(repr(Never), 'typing.Never')
+
+
+class AssertNeverTests(BaseTestCase):
+    def test_exception(self):
+        with self.assertRaises(AssertionError):
+            assert_never(None)
 
 
 class SelfTests(BaseTestCase):
diff --git a/Lib/typing.py b/Lib/typing.py
index 0ee5c8596a021..1de48cca00d84 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -5,7 +5,7 @@
 * Imports and exports, all public names should be explicitly added to __all__.
 * Internal helper functions: these should never be used in code outside this module.
 * _SpecialForm and its instances (special forms):
-  Any, NoReturn, ClassVar, Union, Optional, Concatenate
+  Any, NoReturn, Never, ClassVar, Union, Optional, Concatenate
 * Classes whose instances can be type arguments in addition to types:
   ForwardRef, TypeVar and ParamSpec
 * The core of internal generics API: _GenericAlias and _VariadicGenericAlias, the latter is
@@ -117,12 +117,14 @@ def _idfunc(_, x):
 
     # One-off things.
     'AnyStr',
+    'assert_never',
     'cast',
     'final',
     'get_args',
     'get_origin',
     'get_type_hints',
     'is_typeddict',
+    'Never',
     'NewType',
     'no_type_check',
     'no_type_check_decorator',
@@ -175,7 +177,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms=
     if (isinstance(arg, _GenericAlias) and
             arg.__origin__ in invalid_generic_forms):
         raise TypeError(f"{arg} is not valid as type argument")
-    if arg in (Any, NoReturn, Self, ClassVar, Final, TypeAlias):
+    if arg in (Any, NoReturn, Never, Self, ClassVar, Final, TypeAlias):
         return arg
     if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol):
         raise TypeError(f"Plain {arg} is not valid as type argument")
@@ -441,8 +443,39 @@ def NoReturn(self, parameters):
       def stop() -> NoReturn:
           raise Exception('no way')
 
-    This type is invalid in other positions, e.g., ``List[NoReturn]``
-    will fail in static type checkers.
+    NoReturn can also be used as a bottom type, a type that
+    has no values. Starting in Python 3.11, the Never type should
+    be used for this concept instead. Type checkers should treat the two
+    equivalently.
+
+    """
+    raise TypeError(f"{self} is not subscriptable")
+
+# This is semantically identical to NoReturn, but it is implemented
+# separately so that type checkers can distinguish between the two
+# if they want.
+ at _SpecialForm
+def Never(self, parameters):
+    """The bottom type, a type that has no members.
+
+    This can be used to define a function that should never be
+    called, or a function that never returns::
+
+        from typing import Never
+
+        def never_call_me(arg: Never) -> None:
+            pass
+
+        def int_or_str(arg: int | str) -> None:
+            never_call_me(arg)  # type checker error
+            match arg:
+                case int():
+                    print("It's an int")
+                case str():
+                    print("It's a str")
+                case _:
+                    never_call_me(arg)  # ok, arg is of type Never
+
     """
     raise TypeError(f"{self} is not subscriptable")
 
@@ -2060,6 +2093,29 @@ class Film(TypedDict):
     return isinstance(tp, _TypedDictMeta)
 
 
+def assert_never(arg: Never, /) -> Never:
+    """Statically assert that a line of code is unreachable.
+
+    Example::
+
+        def int_or_str(arg: int | str) -> None:
+            match arg:
+                case int():
+                    print("It's an int")
+                case str():
+                    print("It's a str")
+                case _:
+                    assert_never(arg)
+
+    If a type checker finds that a call to assert_never() is
+    reachable, it will emit an error.
+
+    At runtime, this throws an exception when called.
+
+    """
+    raise AssertionError("Expected code to be unreachable")
+
+
 def no_type_check(arg):
     """Decorator to indicate that annotations are not type hints.
 
diff --git a/Misc/NEWS.d/next/Library/2022-01-23-15-35-07.bpo-46475.UCe18S.rst b/Misc/NEWS.d/next/Library/2022-01-23-15-35-07.bpo-46475.UCe18S.rst
new file mode 100644
index 0000000000000..99d5e2b42c4f6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-01-23-15-35-07.bpo-46475.UCe18S.rst
@@ -0,0 +1,2 @@
+Add :data:`typing.Never` and :func:`typing.assert_never`. Patch by Jelle
+Zijlstra.



More information about the Python-checkins mailing list