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:
foo.value
is somethingfoo.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)
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.
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?
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 of TypeGuard[int]
such that type checkers like mypy
will infer type of int
for values 'type guarded' under calls to this function (e.g., an assert
of if
expression).
from typing import TypeGuard, Any
# ...
def _value_is_set(value: Any) -> TypeGuard[int]:
if isinstance(value, int):
return True
return False
def test_foo():
foo = Foo()
assert foo.value is None
foo.set_value(1)
assert _value_is_set(foo.value)
# the next line is redundant now, but can be kept without issue
assert isinstnace(foo.value, int)
assert foo.value == 1 # now reachable, according to mypy
Normally, mypy
should treat assert isinstance(...)
or if isinstance(...)
in a similar way. But for whatever reason, it doesn't in this case. Using TypeGuard
, we can coarse type checkers into doing the correct thing.
With this change applied, mypy will not think this code is unreachable.