python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.26k stars 2.79k forks source link

Checking Generic Type at Runtime #13053

Open samuelstevens opened 2 years ago

samuelstevens commented 2 years ago

Bug Report

When checking the type of a generic, mypy reports an error when (as far as I understand), I am still following the type contract I set.

To Reproduce

from typing import TypeVar

T = TypeVar("T")

def increment(obj: T) -> T:
    if isinstance(obj, int):
        return obj + 1
    else:
        return obj
$ mypy --strict scratch.py
scratch.py:12: error: Incompatible return value type (got "int", expected "T")
Found 1 error in 1 file (checked 1 source file)

Expected Behavior

I expect there to be no error because if obj is an int, then returning an int is the correct behavior.

Actual Behavior

Mypy reports an error.

Your Environment

erictraut commented 2 years ago

This behavior is correct because isinstance(obj, int) does not guarantee that obj is an int; it simply guarantees that it's a subtype of int. However, the __add__ method for int always returns an int, not a subtype of int.

class IntSubclass(int): ...

i1 = IntSubclass(0)
i2 = increment(i1)

# At runtime, i2 is an instance of 'int', not 'IntSubclass'.
print(type(i2)) # <class 'int'>
samuelstevens commented 2 years ago

That makes sense. Thank you for explainging!

My actual use case is more complicated, however. Instead of int, I have some objects of a class that call some methods. Something like:

from typing import TypeVar

T = TypeVar("T")

class Dummy:
    def __init__(self):
        self.state: int = 0

    def mutate_state(self):
        self.state += 1

def handle(obj: T) -> T:
    if isinstance(obj, Dummy):
        obj.mutate_state()
        return obj
    else:
        return obj

I get the same error:

$ mypy --strict scratch.py
scratch.py:17: error: Incompatible return value type (got "Dummy", expected "T")
Found 1 error in 1 file (checked 1 source file)

Even though obj might just be a subclass of Dummy, I am still returning obj directly which is still of type T. So shouldn't this be error-free?

erictraut commented 2 years ago

Yes, your revised sample is type safe. I don't think mypy should emit an error in this case. By comparison, pyright does not.

Here's a workaround for mypy:

@overload
def handle(obj: Dummy) -> Dummy: ...
@overload
def handle(obj: T) -> T: ...

def handle(obj: T | Dummy) -> T | Dummy:
    if isinstance(obj, Dummy):
        obj.mutate_state()
        return obj
    else:
        return obj
samuelstevens commented 2 years ago

Thanks for the suggestion; I will try it in my full case but I was hoping to avoid all the extra boilerplate! (I have half a dozen classes that have special isinstance cases.)

I'll leave this open as a bug in mypy. If I have time, maybe I will be able to look into why this happens (probably not).