python/mypy

Classmethod constructor pattern on a generic container with a bound typevar has incompatible type with fixed output type

Closed this issue · 3 comments

Bug Report

Using a constructor pattern with cls instead of the explicit class term in a generic context gives a type error.

To Reproduce

from dataclasses import dataclass

from typing import Generic, TypeVar

@dataclass
class X:
    pass

@dataclass
class Y:
    pass

T = TypeVar('T', bound=X | Y)

@dataclass
class Container(Generic[T]):
    value: T

    @classmethod
    def create_x(cls, x: X) -> 'Container[X]':
        return Container(x)

    @classmethod
    def create_x_var(cls, x: X) -> 'Container[X]':
        return cls(x)  # incompatible return type, got "Container[T]" expected "Container[X]", Argument 1 to "Container" has incompatible type "X"; expected "T"

x = X()
c = Container.create_x(x)
c2 = Container.create_x_var(x)

Expected Behavior

The cls call is accepted as well.

Actual Behavior

bound_mwe.py:25: error: Incompatible return value type (got "Container[T]", expected "Container[X]")  [return-value]
bound_mwe.py:25: error: Argument 1 to "Container" has incompatible type "X"; expected "T"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: 1.13.0
  • Mypy command-line flags: n/a
  • Mypy configuration options from mypy.ini (and other config files): n/a
  • Python version used: 3.11.8

Hey @pechersky I can work on this issue.. please assign me with this.
And please tell some more about this issue so I can work upon this more accurately

@kanishka-coder0809 we don't generally assign issues, but if you have a solution or ideas, please mention them here. You'll have to investigate further what is going on in the code sample and how mypy understands the code. @pechersky provided a detailed report already.

This is expected. cls and Container cannot be used interchangeably here. You can think of cls as having type type[Self] where Self is a type variable with an upper bound of the current class. That type variable isn't necessarily compatible with Container[X] (e.g. maybe it's a Container[Y]). Nor is X necessarily compatible with the type T is bound to (it could be any specific subtype of X | Y).

It might be clearer why this isn't valid if you consider what happens when you subclass Container:

class Foo(Container[Y]):

f = Foo.create_x_var(X())  # f is a `Foo` at runtime
reveal_type(f)  # Revealed type is "__main__.Container[__main__.X]"
                # uh oh! Runtime type and static type are incompatible.

There are a few ways you can make something like this type safe:

  1. Use Container directly, like in create_x. A staticmethod or free function might make sense in this case, since you don't actually depend on the current class.
  2. Use cls, but return Self instead of Container[X] so that the return type varies appropriately with the current class:
    @classmethod
    def create_x_var(cls, x: X) -> Self:
        return cls(x)
  3. Annotate cls to only accept subtypes of type[Container[X]] (it might make sense to use another type variable to avoid unnecessarily widening the return type):
    @classmethod
    def create_x_var(cls: type[Container[X]], x: X) -> Container[X]:
        return cls(x)
    This will defer type errors to call sites that try to pass an invalid cls argument:
    class FooX(Container[X]): pass
    class FooY(Container[Y]): pass
    
    FooX.create_x_var(X())  # ok
    FooY.create_x_var(X())  # E: Invalid self argument "type[FooY]" to attribute function "create_x_var" with type "Callable[[type[Container[X]], X], Container[X]]"