Skip to content

Commit

Permalink
gh-90633: Improve error and docs for typing.assert_never (#91720)
Browse files Browse the repository at this point in the history
Closes #90633

Co-authored-by: Alex Waygood <[email protected]>
  • Loading branch information
JelleZijlstra and AlexWaygood committed Apr 25, 2022
1 parent 9ff2f12 commit 93d2801
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 2 deletions.
16 changes: 15 additions & 1 deletion Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2345,11 +2345,25 @@ Functions and decorators
case _ as unreachable:
assert_never(unreachable)

Here, the annotations allow the type checker to infer that the
last case can never execute, because ``arg`` is either
an :class:`int` or a :class:`str`, and both options are covered by
earlier cases.
If a type checker finds that a call to ``assert_never()`` is
reachable, it will emit an error.
reachable, it will emit an error. For example, if the type annotation
for ``arg`` was instead ``int | str | float``, the type checker would
emit an error pointing out that ``unreachable`` is of type :class:`float`.
For a call to ``assert_never`` to succeed, the inferred type of
the argument passed in must be the bottom type, :data:`Never`, and nothing
else.

At runtime, this throws an exception when called.

.. seealso::
`Unreachable Code and Exhaustiveness Checking
<https://typing.readthedocs.io/en/latest/source/unreachable.html>_` has more
information about exhaustiveness checking with static typing.

.. versionadded:: 3.11

.. function:: reveal_type(obj)
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,19 @@ def test_exception(self):
with self.assertRaises(AssertionError):
assert_never(None)

value = "some value"
with self.assertRaisesRegex(AssertionError, value):
assert_never(value)

# Make sure a huge value doesn't get printed in its entirety
huge_value = "a" * 10000
with self.assertRaises(AssertionError) as cm:
assert_never(huge_value)
self.assertLess(
len(cm.exception.args[0]),
typing._ASSERT_NEVER_REPR_MAX_LENGTH * 2,
)


class SelfTests(BaseTestCase):
def test_equality(self):
Expand Down
8 changes: 7 additions & 1 deletion Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2382,6 +2382,9 @@ class Film(TypedDict):
return isinstance(tp, _TypedDictMeta)


_ASSERT_NEVER_REPR_MAX_LENGTH = 100


def assert_never(arg: Never, /) -> Never:
"""Statically assert that a line of code is unreachable.
Expand All @@ -2402,7 +2405,10 @@ def int_or_str(arg: int | str) -> None:
At runtime, this throws an exception when called.
"""
raise AssertionError("Expected code to be unreachable")
value = repr(arg)
if len(value) > _ASSERT_NEVER_REPR_MAX_LENGTH:
value = value[:_ASSERT_NEVER_REPR_MAX_LENGTH] + '...'
raise AssertionError(f"Expected code to be unreachable, but got: {value}")


def no_type_check(arg):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Include the passed value in the exception thrown by
:func:`typing.assert_never`. Patch by Jelle Zijlstra.

0 comments on commit 93d2801

Please sign in to comment.