Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-46475: Add typing.Never and typing.assert_never #30842

Merged
merged 13 commits into from
Feb 8, 2022
59 changes: 59 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,37 @@ 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::
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved

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

def stop() -> Never:
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
raise RuntimeError('no way')

.. 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
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved

Special type indicating that a function never returns.
Expand All @@ -582,6 +613,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

Expand Down Expand Up @@ -1932,6 +1969,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.
Expand Down
48 changes: 33 additions & 15 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,38 +123,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(RuntimeError):
assert_never(None)


class TypeVarTests(BaseTestCase):
Expand Down
69 changes: 65 additions & 4 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -174,7 +176,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, ClassVar, Final, TypeAlias):
if arg in (Any, NoReturn, Never, 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")
Expand Down Expand Up @@ -440,8 +442,44 @@ 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):
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
"""The bottom type, a type that has no members.

The `bottom type <https://en.wikipedia.org/wiki/Bottom_type>`_,
a type that has no members.
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved

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

def stop() -> Never:
raise RuntimeError('no way')
"""
raise TypeError(f"{self} is not subscriptable")

Expand Down Expand Up @@ -1950,6 +1988,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.
Comment on lines +2091 to +2101
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is much better than the examples in the main doc. :-) And so is the sentence following the example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sentence is also in the docs for assert_never, but not Never. I'm pushing some changes to improve the Never docs according to your feedback.

Honestly I'm a bit confused about the relationship between the docstring and the RST docs. I end up copying from one to the other.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually the docstring is shorter, since it is often displayed by tools that have limited output space (it's really annoying to get a scroll bar in a tooltip in VS Code :-).


At runtime, this throws an exception when called.

"""
raise RuntimeError("Expected code to be unreachable")
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved


def no_type_check(arg):
"""Decorator to indicate that annotations are not type hints.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :data:`typing.Never` and :func:`typing.assert_never`. Patch by Jelle
Zijlstra.