Joshua Gilman
Joshua Gilman

Reputation: 1202

mypy fails to detect conditional

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

Answers (4)

hussic
hussic

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

Silvio Mayolo
Silvio Mayolo

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

chepner
chepner

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

Lukasz Wiecek
Lukasz Wiecek

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

Related Questions