Skip to content

Commit 93d2801

Browse files
gh-90633: Improve error and docs for typing.assert_never (#91720)
Closes #90633 Co-authored-by: Alex Waygood <[email protected]>
1 parent 9ff2f12 commit 93d2801

File tree

4 files changed

+37
-2
lines changed

4 files changed

+37
-2
lines changed

Doc/library/typing.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2345,11 +2345,25 @@ Functions and decorators
23452345
case _ as unreachable:
23462346
assert_never(unreachable)
23472347

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

23512360
At runtime, this throws an exception when called.
23522361

2362+
.. seealso::
2363+
`Unreachable Code and Exhaustiveness Checking
2364+
<https://typing.readthedocs.io/en/latest/source/unreachable.html>_` has more
2365+
information about exhaustiveness checking with static typing.
2366+
23532367
.. versionadded:: 3.11
23542368

23552369
.. function:: reveal_type(obj)

Lib/test/test_typing.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,19 @@ def test_exception(self):
223223
with self.assertRaises(AssertionError):
224224
assert_never(None)
225225

226+
value = "some value"
227+
with self.assertRaisesRegex(AssertionError, value):
228+
assert_never(value)
229+
230+
# Make sure a huge value doesn't get printed in its entirety
231+
huge_value = "a" * 10000
232+
with self.assertRaises(AssertionError) as cm:
233+
assert_never(huge_value)
234+
self.assertLess(
235+
len(cm.exception.args[0]),
236+
typing._ASSERT_NEVER_REPR_MAX_LENGTH * 2,
237+
)
238+
226239

227240
class SelfTests(BaseTestCase):
228241
def test_equality(self):

Lib/typing.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2382,6 +2382,9 @@ class Film(TypedDict):
23822382
return isinstance(tp, _TypedDictMeta)
23832383

23842384

2385+
_ASSERT_NEVER_REPR_MAX_LENGTH = 100
2386+
2387+
23852388
def assert_never(arg: Never, /) -> Never:
23862389
"""Statically assert that a line of code is unreachable.
23872390
@@ -2402,7 +2405,10 @@ def int_or_str(arg: int | str) -> None:
24022405
At runtime, this throws an exception when called.
24032406
24042407
"""
2405-
raise AssertionError("Expected code to be unreachable")
2408+
value = repr(arg)
2409+
if len(value) > _ASSERT_NEVER_REPR_MAX_LENGTH:
2410+
value = value[:_ASSERT_NEVER_REPR_MAX_LENGTH] + '...'
2411+
raise AssertionError(f"Expected code to be unreachable, but got: {value}")
24062412

24072413

24082414
def no_type_check(arg):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Include the passed value in the exception thrown by
2+
:func:`typing.assert_never`. Patch by Jelle Zijlstra.

0 commit comments

Comments
 (0)