Problem description
Suppose a following test
class Foo:
def __init__(self):
self.value: int | None = None
def set_value(self, value: int | None):
self.value = value
def test_foo():
foo = Foo()
assert foo.value is None
foo.set_value(1)
assert isinstance(foo.value, int)
assert foo.value == 1 # unreachable
The test:
- First, checks that
foo.value
is something - Then, sets the value using a method.
- Then it checks that the
foo.value
has changed.
When running the test with mypy version 1.9.0 (latest at the time of writing), and having warn_unreachable set to True, one gets:
(venv) niko@niko-ubuntu-home:~/code/myproj$ python -m mypy tests/test_foo.py
tests/test_foo.py:16: error: Statement is unreachable [unreachable]
Found 1 error in 1 file (checked 1 source file)
What I have found
- There is an open issue in the mypy GitHub: https://github.com/python/mypy/issues/11969 One comment said to use safe-assert, but after rewriting the test as
from safe_assert import safe_assert
def test_foo():
foo = Foo()
safe_assert(foo.value is None)
foo.set_value(1)
safe_assert(isinstance(foo.value, int))
assert foo.value == 1
the problem persists (safe-assert 0.4.0). This time, both mypy and VS Code Pylance think that foo.set_value(1)
two lines above is not reachable.
Question
How can I say to mypy that the foo.value
has changed to int
and that it should continue checking also everything under the assert isinstance(foo.value, int)
line?
2
Answers
While writing the question and playing around with
safe_assert
I noticed that the v. 0.4.0 uses NoReturn as the return type. Removing that makes mypy happy. So in essence, one may use this assafe_assert
:You can explicitly control type narrowing with the TypeGuard special form (PEP 647). Although normally you would use
TypeGuard
to farther narrow a type than what has already been inferred, you can use it to ‘narrow’ to whatever type you choose, even if it is different or broader than the type checker has already inferred.In this case, we’ll write a function
_value_is_set
which is annotated with a return type ofTypeGuard[int]
such that type checkers likemypy
will infer type ofint
for values ‘type guarded’ under calls to this function (e.g., anassert
ofif
expression).Normally,
mypy
should treatassert isinstance(...)
orif isinstance(...)
in a similar way. But for whatever reason, it doesn’t in this case. UsingTypeGuard
, we can coarse type checkers into doing the correct thing.With this change applied, mypy will not think this code is unreachable.