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.valueis something - Then, sets the value using a method.
- Then it checks that the
foo.valuehas 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?
You can explicitly control type narrowing with the TypeGuard special form (PEP 647). Although normally you would use
TypeGuardto 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_setwhich is annotated with a return type ofTypeGuard[int]such that type checkers likemypywill infer type ofintfor values 'type guarded' under calls to this function (e.g., anassertofifexpression).Normally,
mypyshould 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.