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:
- Use
Container
directly, like increate_x
. A staticmethod or free function might make sense in this case, since you don't actually depend on the current class. - Use
cls
, but returnSelf
instead ofContainer[X]
so that the return type varies appropriately with the current class:@classmethod def create_x_var(cls, x: X) -> Self: return cls(x)
- Annotate
cls
to only accept subtypes oftype[Container[X]]
(it might make sense to use another type variable to avoid unnecessarily widening the return type):This will defer type errors to call sites that try to pass an invalid@classmethod def create_x_var(cls: type[Container[X]], x: X) -> Container[X]: return cls(x)
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]]"