Skip to content

Error inferring typevar on generic classes #18706

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

Closed
alfred82santa opened this issue Feb 19, 2025 · 6 comments · Fixed by #18943
Closed

Error inferring typevar on generic classes #18706

alfred82santa opened this issue Feb 19, 2025 · 6 comments · Fixed by #18943
Labels
bug mypy got something wrong

Comments

@alfred82santa
Copy link

Bug Report

The inferred type of methods of a generic class are not bounded to typevar value on descendant classes.

To Reproduce

from typing import Iterator, Generic, TypeVar

T = TypeVar("T", str, int)

class Parent(Generic[T]):

    @classmethod
    def method_1(cls, value: T) -> T:
        return 2*value
        
    def method_2(self, value: T) -> T:
        return 2*value
        
class ChildInt(Parent[int]):
    pass

instance = ChildInt()

reveal_type(instance.method_1)  # Revealed type is "def (value: builtins.int) -> builtins.int"
reveal_type(ChildInt.method_1)  # Revealed type is "def (value: builtins.int) -> builtins.int"

reveal_type(instance.method_2)  # Revealed type is "def (value: builtins.int) -> builtins.int"
reveal_type(ChildInt.method_2)  # Revealed type is "def (self: __main__.Parent[T`1], value: T`1) -> T`1"

https://mypy-play.net/?mypy=latest&python=3.12&gist=7ce2739ccc2b43f37187d54dcdd5f923

Expected Behavior

reveal_type(ChildInt.method_2)  # Revealed type is "def (self: __main__.ChildInt, value: builtins.int) -> builtins.int"

This soulution could be acceptable, as well:

reveal_type(ChildInt.method_2)  # Revealed type is "def (self: __main__.Parent[builtins.int], value: builtins.int) -> builtins.int"

Actual Behavior

reveal_type(ChildInt.method_2)  # Revealed type is "def (self: __main__.Parent[T`1], value: T`1) -> T`1"

Your Environment

  • Mypy version used: 1.15
  • Mypy command-line flags: no flags
  • Mypy configuration options from mypy.ini (and other config files): no config
  • Python version used: 3.13

Releated issues

#15658

@alfred82santa alfred82santa added the bug mypy got something wrong label Feb 19, 2025
@A5rocks
Copy link
Collaborator

A5rocks commented Feb 19, 2025

Does this cause any false positives or false negatives? Mypy should eventually bind self and then at that point it will find what T is. (I don't know how to interpret the related issue)

@alfred82santa
Copy link
Author

alfred82santa commented Feb 19, 2025

The related problem was that T was not bound to the type in an instance object. The current problem is that T is not bound to the type in the class object.

It could produce false negative if the class method is referenced. I know it is not common and a little bit strange.

from typing import Iterator, Generic, TypeVar, Callable, Any

T = TypeVar("T", str, int)

class Parent(Generic[T]):

    @classmethod
    def method_1(cls, value: T) -> T:
        return 2*value
        
    def method_2(self, value: T) -> T:
        return 2*value
        
class ChildInt(Parent[int]):
    pass

instance = ChildInt()

reveal_type(ChildInt.method_1)  # Revealed type is "def (value: builtins.int) -> builtins.int"
reveal_type(instance.method_1)  # Revealed type is "def (value: builtins.int) -> builtins.int"

reveal_type(ChildInt.method_2)  # Revealed type is "def (self: __main__.Parent[T`1], value: T`1) -> T`1"
reveal_type(instance.method_2)  # Revealed type is "def (value: builtins.int) -> builtins.int"

def decorator[S](f: Callable[[S, int], int]) -> Callable[[S, int], int]:
    return f
    
    
class GrandChildInt(ChildInt):
    
    method_3 = decorator(ChildInt.method_2)  # error: Argument 1 to "decorator" has incompatible type "Callable[[Parent[T], T], T]"; expected "Callable[[Parent[T], int], int]"  [arg-type]

https://mypy-play.net/?mypy=latest&python=3.12&gist=7ce2739ccc2b43f37187d54dcdd5f923

@sterliakov
Copy link
Collaborator

This is another branch missing in checkmember.py: analyze_class_attribute_access (and precisely add_class_tvars?) does not map instance methods properly. A map_self helper similar to bind_self should help, do we have it somewhere? In all other cases using checker.bind_and_map_method and its helpers suffices, but here we do need to preserve the type of the first argument. (it isn't as trivial as "replace with current self type" due to self-type constraints on overloads and mapping generics)

@A5rocks
Copy link
Collaborator

A5rocks commented Feb 22, 2025

infer_type_arguments with isuper but polymorphic? Like something like this: https://github.com/python/mypy/pull/18401/files#diff-1dce7e2a19559e2ff4e08fd46877b87c9cfec3cb5e6d465157788542948e11e4R369-R370 (not sure if that would work if isuper is still generic like list[T] and not list[str]...)

@sterliakov
Copy link
Collaborator

That's what bind_self does, yep, I hoped there's something higher-level somewhere:)

cdce8p pushed a commit to cdce8p/mypy that referenced this issue May 31, 2025
Fixes python#18024
Fixes python#18706
Fixes python#17734
Fixes python#15097
Fixes python#14814
Fixes python#14806
Fixes python#14259
Fixes python#13041
Fixes python#11993
Fixes python#9585
Fixes python#9266
Fixes python#9202
Fixes python#5481

This is a fourth "major" PR toward
python#7724. This is one is
watershed/crux of the whole series (but to set correct expectations,
there are almost a dozen smaller follow-up/clean-up PRs in the
pipeline).

The core of the idea is to set current type-checker as part of the
global state. There are however some details:
* There are cases where we call `is_subtype()` before type-checking. For
now, I fall back to old logic in this cases. In follow up PRs we may
switch to using type-checker instances before type checking phase (this
requires some care).
* This increases typeops import cycle by a few modules, but
unfortunately this is inevitable.
* This PR increases potential for infinite recursion in protocols. To
mitigate I add: one legitimate fix for `__call__`, and one temporary
hack for `freshen_all_functions_type_vars` (to reduce performance
impact).
* Finally I change semantics for method access on class objects to match
the one in old `find_member()`. Now we will expand type by instance, so
we have something like this:
  ```python
  class B(Generic[T]):
      def foo(self, x: T) -> T: ...
  class C(B[str]): ...
  reveal_type(C.foo)  # def (self: B[str], x: str) -> str
  ```
FWIW, I am not even 100% sure this is correct, it seems to me we _may_
keep the method generic. But in any case what we do currently is
definitely wrong (we infer a _non-generic_ `def (x: T) -> T`).

---------

Co-authored-by: hauntsaninja <[email protected]>
Co-authored-by: Shantanu <[email protected]>
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

Successfully merging a pull request may close this issue.

3 participants