Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
pechersky opened this issue Dec 4, 2024 · 3 comments
Labels
bug mypy got something wrong

Comments

@pechersky
Copy link

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
@pechersky pechersky added the bug mypy got something wrong label Dec 4, 2024
@kanishka-coder0809
Copy link

kanishka-coder0809 commented Dec 4, 2024

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

@JelleZijlstra
Copy link
Member

@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.

@brianschubert
Copy link
Collaborator

brianschubert commented Dec 4, 2024

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]]"

@brianschubert brianschubert closed this as not planned Won't fix, can't repro, duplicate, stale Dec 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

4 participants