Skip to content

Commit

Permalink
bpo-46475: Add typing.Never and typing.assert_never (GH-30842)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Feb 8, 2022
1 parent 1e6214d commit 243436f
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 19 deletions.
56 changes: 56 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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.
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 @@ -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):
Expand Down
64 changes: 60 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 @@ -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")
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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.
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.

0 comments on commit 243436f

Please sign in to comment.