Reputation: 1202
A working example of the issue (Python 3.8):
from typing import Union
class TestA:
def test(self) -> str:
return "testa"
class TestB:
def test(self) -> str:
return "testb"
tests = Union[TestA, TestB]
more = Union[TestA, TestB, str]
def test_pass(p: tests) -> str:
return p.test() # No error
def test_fail(p: more) -> str:
if type(p) in [TestA, TestB]:
return p.test() # Item "str" of "Union[TestA, TestB, str]" has no attribute "test"
else:
return ""
print(test_fail(TestA())) # "testa"
print(test_fail(TestB())) # "testb"
print(test_fail("str")) # "str"
I have this exact scenario showing up in a few places in my codebase and it's really bothering me I have to disable type checking on these lines. Is this something mypy
is expected to miss or am I the one in error here? The error being returned seems unreasonable since a str
could never make it to that leg of the conditional.
Upvotes: 5
Views: 1395
Reputation: 1920
You can use a TypeGuard like this:
tests = Union[TestA, TestB]
def is_tests(x) -> TypeGuard[tests]:
return type(x) in (TestA, TestB)
and then:
if is_tests(p):
return p.test()
Upvotes: 0
Reputation: 70367
If your goal is to exclude a single case, mypy will also recognize is not
. But it can't recognize more complex expressions like in
.
if type(p) is not str:
...
Upvotes: 2
Reputation: 532208
mypy
cannot use the runtime value of an expression like type(p) in [TestA, TestB]
to determine that p
is not a str
. You might use typing.overload
instead to provide two different type signatures for type checking: one for tests
, the other for str
.
from typing import overload
@overload
def test_fail(p: tests) -> str:
...
@overload
def test_fail(p: str) -> str:
...
def test_fail(p):
if type(p) in [TestA, TestB]:
return p.test()
else:
return ""
(I am not sure, exactly, how mypy
makes use of these annotations to identify the type(p) in [TestA, TestB]
case with tests
and the else
with str
. This also doesn't seem to work with mypy --strict
, as it will complain about test_fail
not being annotated. The lack of type hints on the actual implementation is intentional and necessary.)
Or, you can simply use typing.cast
to tell mypy
that you know the (more) specific type of p
:
from typing import cast
def test_fail(p: more) -> str:
if type(p) in [TestA, TestB]:
return cast(tests, p).test() # Item "str" of "Union[TestA, TestB, str]" has no attribute "test"
else:
return ""
Upvotes: 2
Reputation: 424
Turns out in
operator won't work here. Try testing with is
instead:
def test_fail(p: more) -> str:
if type(p) is TestA or type(p) is TestB:
...
See https://mypy.readthedocs.io/en/stable/type_narrowing.html#type-narrowing-expressions for available Type Narrowing Expressions.
Upvotes: 3