microsoft / pyright

Static Type Checker for Python
Other
13.32k stars 1.45k forks source link

(Strict mode) cannot re-bind `TypeVar` to `classmethod` #8816

Closed callumforrester closed 2 months ago

callumforrester commented 2 months ago

Describe the bug Apologies if this is an intended feature. I need to define two TypeVars to bind to a class and a classmethod separately in strict mode. I'm unsure if this is a bug or highlights an explicit difference between the two.

Code or Screenshots

Code sample in pyright playground

from typing import TypeVar, Generic
from dataclasses import dataclass

T = TypeVar("T")
U = TypeVar("U")

@dataclass
class FooBar(Generic[T]):
    a: T
    b: int

    @classmethod
    def alternative_constructor_1(cls, a: T, b: int) -> "FooBar[T]":
        return cls(a, b)

    @classmethod
    def alternative_constructor_2(cls, a: U, b: int) -> "FooBar[U]":
        return cls(a, b)  

    @classmethod
    def alternative_constructor_3(cls, a: T, b: int) -> "FooBar[T]":
        return FooBar(a, b)

    @classmethod
    def alternative_constructor_4(cls, a: U, b: int) -> "FooBar[U]":
        return FooBar(a, b)

FooBar_0 = FooBar("time", 0)
FooBar_1 = FooBar.alternative_constructor_1("time", 1)  # This fails because T cannot bind to both FooBar and FooBar.alternative_constructor_1
FooBar_2 = FooBar.alternative_constructor_2("time", 2)  # This fails (above) because cls is already bound to T but FooBar.alternative_constructor_2 is bound to U
FooBar_3 = FooBar.alternative_constructor_3("time", 3)  # This fails because T cannot bind to both FooBar and FooBar.alternative_constructor_1
FooBar_4 = FooBar.alternative_constructor_4("time", 4)  # This succeeds because FooBar.alternative_constructor_3 is bound to U and also re-binds FooBar to U by foregoing the use of cls
erictraut commented 2 months ago

Pyright is working as intended here. In a class method, the implied type for cls (if you don't provide an explicit annotation) is type[Self]. This means it must be assignable to the specialized type of its parent class. In your example, the specialized type of FooBar is FooBar[T]. When you call cls as a constructor, it's like you're calling FooBar[T](a, b). That means the __init__ method gets specialized with the bound value of T. In the call, you're attempting to assign the bound value U to parameter a which has the value of T, and these types are not compatible.

Here's an alternative formulation that avoids the type error:

    @classmethod
    def alternative_constructor_2(cls: "type[FooBar[Any]]", a: U, b: int) -> "FooBar[U]":
        return cls(a, b)

Or

    @classmethod
    def alternative_constructor_2(cls: "type[FooBar[U]]", a: U, b: int) -> "FooBar[U]":
        return cls(a, b)
callumforrester commented 2 months ago

@erictraut makes sense to me, thanks. Just to check I haven't missed something: There is no way to have a single TypeVar, right? I need both T and U?