python / mypy

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

Generic class with constrained type vars #5416

Open wkschwartz opened 6 years ago

wkschwartz commented 6 years ago

On Python 3.7.0 and mypy 0.620, I'm getting some error messages I don't understand. The Mypy documentation and PEP 484 didn't clear it up for me. Maybe it's a bug in Mypy. Maybe I don't understand type erasure. In case it's the former, here's a minimal example. In case it's the latter, please help.

In the following examples, the fact that m does not return is not critical. In the real code that gave me these errors, m is an abstract method, which also doesn't change anything important.

Put the following examples in mypy-test.py to get the corresponding output from running mypyt mypy-test.py. The first and the last examples are the ones that are baffling me.

classmethod with constrained type variable

from typing import TypeVar, Generic
T = TypeVar('T', int, str)
class K(Generic[T]):
    @classmethod
    def m(cls) -> T:
        raise NotImplementedError

Mypy output:

mypy-test.py:5: error: The erased type of self 'def [T in (builtins.int, builtins.str)] () -> mypy-test.K[builtins.int*]' is not a supertype of its class 'Type[mypy-test.K[T`1]]'
mypy-test.py:5: error: The erased type of self 'def [T in (builtins.int, builtins.str)] () -> mypy-test.K[builtins.str*]' is not a supertype of its class 'Type[mypy-test.K[T`1]]'

classmethod with generic self

from typing import TypeVar, Generic
S = TypeVar('S')
T = TypeVar('T', int, str)
class K(Generic[T]):
    @classmethod
    def m(cls: S) -> T:
        raise NotImplementedError

produces no error.

classmethod with unconstrained type variable

from typing import TypeVar, Generic
T = TypeVar('T')
class K(Generic[T]):
    @classmethod
    def m(cls) -> T:
        raise NotImplementedError

produces no error.

instance method with constrained type

from typing import TypeVar, Generic
T = TypeVar('T', int, str)
class K(Generic[T]):
    def __init__(self, t: T) -> None:
        self.t = t

produces

mypy-test.py:5: error: Need type annotation for 't'

instance method with unconstrained type

from typing import TypeVar, Generic
T = TypeVar('T')
class K(Generic[T]):
    def __init__(self, t: T) -> None:
        self.t = t

produces no error.

instance method with constrained type and inheritance

from typing import TypeVar, Generic
T = TypeVar('T', int, str)
class K(Generic[T]):
    def __init__(self, t: T) -> None:
        self.t: T = t

class L(K[T]):
    def __init__(self, t: T) -> None:
        self.u = 1
        super().__init__(t=t)

produces

mypy-test.py:10: error: Argument "t" to "__init__" of "K" has incompatible type "int"; expected "T"
mypy-test.py:10: error: Argument "t" to "__init__" of "K" has incompatible type "str"; expected "T"
ilevkivskyi commented 6 years ago

The first and the last examples are the ones that are baffling me.

Apart from the fact that using a generic class type variable in class methods is typically a bad idea (although not formally wrong), I think the first and last examples are bugs. Both are results of how mypy checks functions with constrained type variables.

Also I think @elazarg might help with the first one if he has time.

wkschwartz commented 6 years ago

Thanks for responding so quickly! I'll # type: ignore those for now.

Apart from the fact that using a generic class type variable in class methods is typically a bad idea (although not formally wrong)

In my actual code, the class method m is part of an abstract base class for wrapping other libraries for a particular type of functionality I need to interface with. Type T parameterizes a common type among Python libraries for my use case; it's usually either an int or str. Other methods in the base class expect inputs of type T, so I figured I should indicate that the input type T on those methods is the same as the output type T from class method m. For this type of use case, why would using a generic class type variable in the class method be a bad idea? Is there another way to handle this use case?

ilevkivskyi commented 6 years ago

For this type of use case, why would using a generic class type variable in the class method be a bad idea?

This is probably fine. However I would anyway try refactoring the code (if possible) to make m a normal instance method. This is because type variables are something tightly associated with instances, not classes (this is however a matter of style, since a subclass can bind the type variable as well).

wkschwartz commented 6 years ago

I would anyway try refactoring the code (if possible) to make m a normal instance method.

My intention with m is that it return a list of available/installed external programs known to the class available to be requested by command-line interfaces or other client code. I need the type variable because this all happens through third-party libraries, each of which refers to these external programs differently (some with strings, others with C enum integers). I could provide m as a static method, but that seemed unnecessarily to constrain future subclasses. I'm not sure how I could do this at all as an instance method: the CLI would have to instantiate an instance with fake data to obtain the information needed to tell users who ask for --help what data they can process. Thoughts?

ilevkivskyi commented 6 years ago

It looks like you are too worried :-) I said typically and if possible. In your situation it is perfectly OK.

wkschwartz commented 6 years ago

I just tested the examples in the original post using mypy 0.630. The first example no longer generates errors. The fourth and last examples continue to generate errors. Maybe related to fixing #5309?