From 86a868fc54c3371f99c47bb404b836da75f88b08 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 22 Jan 2022 17:24:24 -0800 Subject: [PATCH 1/9] bpo-46475: Add typing.Never and typing.assert_never --- Doc/library/typing.rst | 53 +++++++++++++++++ Lib/test/test_typing.py | 42 +++++++++++++- Lib/typing.py | 57 +++++++++++++++++-- .../2022-01-23-15-35-07.bpo-46475.UCe18S.rst | 2 + 4 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-01-23-15-35-07.bpo-46475.UCe18S.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index cb14db90711cff..3a69cd68b465b1 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -572,6 +572,32 @@ 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 + + Notation for 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 + + never_call_me(1) # type checker error + + def stop() -> Never: + return 1 # type checker error + + The :func:`assert_never()` function uses the ``Never`` type to statically + assert that code is unreachable. + + .. 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. @@ -582,6 +608,11 @@ 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 general + `bottom type `_, a type that + has no values. The :data:`Never` type provides a more explicit name for + the same concept. Type checkers should treat the two equivalently. + .. versionadded:: 3.5.4 .. versionadded:: 3.6.2 @@ -1932,6 +1963,28 @@ Functions and decorators runtime we intentionally don't check anything (we want this to be as fast as possible). +.. function:: assert_never(arg, /) + + 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. + + .. versionadded:: 3.11 + .. decorator:: overload The ``@overload`` decorator allows describing functions and methods diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 150d7c081c30b6..64680eb4f6a274 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 @@ -156,6 +156,46 @@ def test_cannot_instantiate(self): type(NoReturn)() +class NeverTests(BaseTestCase): + + def test_never_instance_type_error(self): + with self.assertRaises(TypeError): + isinstance(42, Never) + + def test_never_subclass_type_error(self): + with self.assertRaises(TypeError): + issubclass(Employee, Never) + with self.assertRaises(TypeError): + issubclass(Never, Employee) + + def test_repr(self): + self.assertEqual(repr(Never), 'typing.Never') + + def test_not_generic(self): + with self.assertRaises(TypeError): + Never[int] + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class A(Never): + pass + with self.assertRaises(TypeError): + class A(type(Never)): + pass + + def test_cannot_instantiate(self): + with self.assertRaises(TypeError): + Never() + with self.assertRaises(TypeError): + type(Never)() + + +class AssertNeverTests(BaseTestCase): + def test_exception(self): + with self.assertRaises(RuntimeError): + assert_never(None) + + class TypeVarTests(BaseTestCase): def test_basic_plain(self): diff --git a/Lib/typing.py b/Lib/typing.py index 972b8ba24b27e8..ea6732643a847b 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', @@ -173,7 +175,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, is_class=False): 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, Final): + if arg in (Any, NoReturn, Never, Final): 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,32 @@ 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. + Static type checkers treat this as a general bottom type, + a type with no members. The typing.Never type provides a + more explicit name for that concept. + + """ + raise TypeError(f"{self} is not subscriptable") + +@_SpecialForm +def Never(self, parameters): + """Notation for 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 + + never_call_me(1) # type checker error + + def stop() -> Never: + return 1 # type checker error + + The assert_never() function uses the Never type to statically + assert that code is unreachable. """ raise TypeError(f"{self} is not subscriptable") @@ -1941,6 +1967,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 RuntimeError("Unreachable code") + + 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 00000000000000..99d5e2b42c4f68 --- /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. From 4b227f67689546ec5e1510c9ad34c95992d266d0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 24 Jan 2022 07:00:24 -0800 Subject: [PATCH 2/9] deduplicate tests --- Lib/test/test_typing.py | 60 +++++++++++++---------------------------- 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 64680eb4f6a274..5503e239acaab1 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -122,72 +122,50 @@ def test_any_works_with_alias(self): typing.IO[Any] -class NoReturnTests(BaseTestCase): +class BottomTypeTests: + 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 NeverTests(BaseTestCase): - - def test_never_instance_type_error(self): - with self.assertRaises(TypeError): - isinstance(42, Never) - - def test_never_subclass_type_error(self): - with self.assertRaises(TypeError): - issubclass(Employee, Never) - with self.assertRaises(TypeError): - issubclass(Never, Employee) +class NoReturnTests(BottomTypeTests, BaseTestCase): + bottom_type = NoReturn def test_repr(self): - self.assertEqual(repr(Never), 'typing.Never') + self.assertEqual(repr(NoReturn), 'typing.NoReturn') - def test_not_generic(self): - with self.assertRaises(TypeError): - Never[int] - def test_cannot_subclass(self): - with self.assertRaises(TypeError): - class A(Never): - pass - with self.assertRaises(TypeError): - class A(type(Never)): - pass +class NeverTests(BottomTypeTests, BaseTestCase): + bottom_type = Never - def test_cannot_instantiate(self): - with self.assertRaises(TypeError): - Never() - with self.assertRaises(TypeError): - type(Never)() + def test_repr(self): + self.assertEqual(repr(Never), 'typing.Never') class AssertNeverTests(BaseTestCase): From d3cc0f5456f35a94103ec01b0886b59bb6ed8bf9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 31 Jan 2022 20:48:47 -0800 Subject: [PATCH 3/9] Address David Foster's feedback --- Doc/library/typing.rst | 4 ++-- Lib/typing.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index ef4c40aded2b4e..1933c430876adb 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1975,8 +1975,8 @@ Functions and decorators print("It's an int") case str(): print("It's a str") - case _: - assert_never(arg) + case _ as unreachable: + assert_never(unreachable) If a type checker finds that a call to ``assert_never()`` is reachable, it will emit an error. diff --git a/Lib/typing.py b/Lib/typing.py index 9c1d217dba7085..baafefae3ca3f0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1995,7 +1995,7 @@ def int_or_str(arg: int | str) -> None: At runtime, this throws an exception when called. """ - raise RuntimeError("Unreachable code") + raise RuntimeError("Expected code to be unreachable") def no_type_check(arg): From 1b297febc9ea2ec361008e45227f3b404e8f7486 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 1 Feb 2022 19:30:08 -0800 Subject: [PATCH 4/9] rename BottomTypeTests --- Lib/test/test_typing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index e759bbd102f200..95560addfb3f55 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -123,7 +123,7 @@ def test_any_works_with_alias(self): typing.IO[Any] -class BottomTypeTests: +class BottomTypeTestsMixin: bottom_type: ClassVar[Any] def test_instance_type_error(self): @@ -155,14 +155,14 @@ def test_cannot_instantiate(self): type(self.bottom_type)() -class NoReturnTests(BottomTypeTests, BaseTestCase): +class NoReturnTests(BottomTypeTestsMixin, BaseTestCase): bottom_type = NoReturn def test_repr(self): self.assertEqual(repr(NoReturn), 'typing.NoReturn') -class NeverTests(BottomTypeTests, BaseTestCase): +class NeverTests(BottomTypeTestsMixin, BaseTestCase): bottom_type = Never def test_repr(self): From b3f4ccb7304f6947cc931856d983398b43ede8c6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 6 Feb 2022 16:37:02 -0800 Subject: [PATCH 5/9] Guido's feedback --- Doc/library/typing.rst | 24 +++++++++++++++--------- Lib/typing.py | 38 +++++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index b84844c16acc01..05a174ae623de8 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -585,13 +585,18 @@ These can be used as types in annotations and do not support ``[]``. def never_call_me(arg: Never) -> None: pass - never_call_me(1) # type checker error + 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 def stop() -> Never: - return 1 # type checker error - - The :func:`assert_never()` function uses the ``Never`` type to statically - assert that code is unreachable. + raise RuntimeError('no way') .. versionadded:: 3.11 @@ -608,10 +613,11 @@ 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 general + ``NoReturn`` can also be used as a `bottom type `_, a type that - has no values. The :data:`Never` type provides a more explicit name for - the same concept. Type checkers should treat the two equivalently. + 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 @@ -1965,7 +1971,7 @@ Functions and decorators .. function:: assert_never(arg, /) - Statically assert that a line of code is unreachable. + Assert to the type checker that a line of code is unreachable. Example:: diff --git a/Lib/typing.py b/Lib/typing.py index a86a5e28745b6f..a2b5d113041a2d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -442,32 +442,44 @@ def NoReturn(self, parameters): def stop() -> NoReturn: raise Exception('no way') - Static type checkers treat this as a general bottom type, - a type with no members. The typing.Never type provides a - more explicit name for that concept. + 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. @_SpecialForm def Never(self, parameters): - """Notation for the bottom type, a type that has no members. + """The bottom type, a type that has no members. + + 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 + from typing import Never - never_call_me(1) # type checker error + def never_call_me(arg: Never) -> None: + pass - def stop() -> Never: - return 1 # type checker error + 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 - The assert_never() function uses the Never type to statically - assert that code is unreachable. + def stop() -> Never: + raise RuntimeError('no way') """ raise TypeError(f"{self} is not subscriptable") From 2530bd75359914531e75a2eee0f009420f3e6948 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 6 Feb 2022 16:44:07 -0800 Subject: [PATCH 6/9] Remove another "Notation for" --- Doc/library/typing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 05a174ae623de8..518f3d27b4c7d3 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -574,7 +574,7 @@ These can be used as types in annotations and do not support ``[]``. .. data:: Never - Notation for the `bottom type `_, + The `bottom type `_, a type that has no members. This can be used to define a function that should never be From c23cc26bab2b5ca73604e3417104626147b1dee2 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 6 Feb 2022 16:54:08 -0800 Subject: [PATCH 7/9] Thanks Alex --- Doc/library/typing.rst | 3 --- Lib/typing.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 518f3d27b4c7d3..cebceb9fa55624 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -595,9 +595,6 @@ These can be used as types in annotations and do not support ``[]``. case _: never_call_me(arg) # ok, arg is of type Never - def stop() -> Never: - raise RuntimeError('no way') - .. versionadded:: 3.11 On older Python versions, :data:`NoReturn` may be used to express the diff --git a/Lib/typing.py b/Lib/typing.py index a2b5d113041a2d..d00a0bbc04218a 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -457,9 +457,6 @@ def stop() -> NoReturn: def Never(self, parameters): """The bottom type, a type that has no members. - 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 4497a26ff1394c04f6c901d25d7128e4d6114e49 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 7 Feb 2022 19:35:51 -0800 Subject: [PATCH 8/9] remove extra example --- Lib/typing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index d00a0bbc04218a..9674d136135236 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -475,8 +475,6 @@ def int_or_str(arg: int | str) -> None: case _: never_call_me(arg) # ok, arg is of type Never - def stop() -> Never: - raise RuntimeError('no way') """ raise TypeError(f"{self} is not subscriptable") From 1f6b671e2f556f14d3f15562e67de3c00627a2d0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 7 Feb 2022 20:12:01 -0800 Subject: [PATCH 9/9] raise AssertionError --- Lib/test/test_typing.py | 2 +- Lib/typing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index e3203dfe448c44..455abe3a1097dd 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -172,7 +172,7 @@ def test_repr(self): class AssertNeverTests(BaseTestCase): def test_exception(self): - with self.assertRaises(RuntimeError): + with self.assertRaises(AssertionError): assert_never(None) diff --git a/Lib/typing.py b/Lib/typing.py index caf94da38c5717..748fb375bd2168 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2103,7 +2103,7 @@ def int_or_str(arg: int | str) -> None: At runtime, this throws an exception when called. """ - raise RuntimeError("Expected code to be unreachable") + raise AssertionError("Expected code to be unreachable") def no_type_check(arg):