diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 8c1c34e90507f4..94a46b01a1a8c0 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 `_, + 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 `_, 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 a37bb432969293..455abe3a1097dd 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 d1d513062394cb..748fb375bd2168 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. +@_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") @@ -2050,6 +2083,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 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.