Skip to content

Invalid argument type when overload self uses a different TypeVar than the class definition #3277

@hoxbro

Description

@hoxbro

When a generic class is defined with one TypeVar('T') but its __init__ overloads use a different TypeVar('CT') in the self-type annotation,

Playground: https://play.ty.dev/05042e22-0dc4-48c8-8899-60539b5e23fb

Both pyright, pyrefly, and mypy report this as what I expect, see below.

Minimal Reproduction

from __future__ import annotations

import typing as t
from typing_extensions import assert_type, reveal_type

T = t.TypeVar("T")
CT = t.TypeVar("CT")


class ClassSelector(t.Generic[T]):
    if t.TYPE_CHECKING:

        @t.overload
        def __init__(
            self: ClassSelector[CT],
            *,
            default: CT,
            class_: type[CT],
        ) -> None: ...

        @t.overload
        def __init__(
            self: ClassSelector[CT | None],
            *,
            default: None = None,
            class_: type[CT],
        ) -> None: ...

    def __init__(self, *, default=None, class_=None): ...


class MyClass:
    pass


a = ClassSelector(default=MyClass(), class_=MyClass)
assert_type(a, "ClassSelector[MyClass]")
reveal_type(a)

b = ClassSelector(class_=MyClass)
assert_type(b, "ClassSelector[MyClass | None]")
reveal_type(b)

Expected Behavior

Revealed type: ClassSelector[MyClass]
Revealed type: ClassSelector[MyClass | None]

Actual Behavior

ty

info[revealed-type]: Revealed type
  --> mre_minimal.py:38:13
   |
36 | a = ClassSelector(default=MyClass(), class_=MyClass)
37 | assert_type(a, "ClassSelector[MyClass]")
38 | reveal_type(a)
   |             ^ `ClassSelector[MyClass]`
39 |
40 | b = ClassSelector(class_=MyClass)
   |

error[invalid-argument-type]: Argument to bound method `__init__` is incorrect
  --> mre_minimal.py:40:5
   |
38 | reveal_type(a)
39 |
40 | b = ClassSelector(class_=MyClass)
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Expected `ClassSelector[None | MyClass]`, found `ClassSelector[None]`
41 | assert_type(b, "ClassSelector[MyClass | None]")
42 | reveal_type(b)
   |
info: Matching overload defined here
  --> mre_minimal.py:22:13
   |
21 |         @t.overload
22 |         def __init__(
   |             ^^^^^^^^
23 |             self: ClassSelector[CT | None],
   |             ------------------------------ Parameter declared here
24 |             *,
25 |             default: None = None,
   |
info: Non-matching overloads for bound method `__init__`:
info:   [CT](self: ClassSelector[CT], *, default: CT, class_: type[CT]) -> None
info: `ClassSelector` is invariant in its type parameter
info: For more information, see https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics
info: rule `invalid-argument-type` is enabled by default

error[type-assertion-failure]: Argument does not have asserted type `ClassSelector[MyClass | None]`
  --> mre_minimal.py:41:1
   |
40 | b = ClassSelector(class_=MyClass)
41 | assert_type(b, "ClassSelector[MyClass | None]")
   | ^^^^^^^^^^^^-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |             |
   |             Inferred type is `ClassSelector[T@ClassSelector]`
42 | reveal_type(b)
   |
info: `ClassSelector[MyClass | None]` and `ClassSelector[T@ClassSelector]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default

info[revealed-type]: Revealed type
  --> mre_minimal.py:42:13
   |
40 | b = ClassSelector(class_=MyClass)
41 | assert_type(b, "ClassSelector[MyClass | None]")
42 | reveal_type(b)
   |             ^ `ClassSelector[T@ClassSelector]`
   |

Found 4 diagnostics

pyright:

/home/shh/projects/holoviz/repos/param/mre_minimal.py
  /home/shh/projects/holoviz/repos/param/mre_minimal.py:38:13 - information: Type of "a" is "ClassSelector[MyClass]"
  /home/shh/projects/holoviz/repos/param/mre_minimal.py:42:13 - information: Type of "b" is "ClassSelector[MyClass | None]"
0 errors, 0 warnings, 2 informations

pyrefly

 INFO revealed type: ClassSelector[MyClass] [reveal-type]
  --> mre_minimal.py:38:12
   |
38 | reveal_type(a)
   |            ---
   |
 INFO revealed type: ClassSelector[MyClass | None] [reveal-type]
  --> mre_minimal.py:42:12
   |
42 | reveal_type(b)
   |            ---
   |
 INFO 0 errors

mypy

mre_minimal.py:38: note: Revealed type is "mre_minimal.ClassSelector[mre_minimal.MyClass]"
mre_minimal.py:42: note: Revealed type is "mre_minimal.ClassSelector[mre_minimal.MyClass | None]"
Success: no issues found in 1 source file

Version

ty 0.0.29

Note:

If I change CT to T it works for ty, but makes other type checker fails:

+ pyright mre_minimal2.py
/home/shh/projects/holoviz/repos/param/mre_minimal2.py
  /home/shh/projects/holoviz/repos/param/mre_minimal2.py:14:19 - warning: Type annotation for "self" parameter of "__init__" method cannot contain class-scoped type variables (reportInvalidTypeVarUse)
  /home/shh/projects/holoviz/repos/param/mre_minimal2.py:22:19 - warning: Type annotation for "self" parameter of "__init__" method cannot contain class-scoped type variables (reportInvalidTypeVarUse)
  /home/shh/projects/holoviz/repos/param/mre_minimal2.py:37:13 - information: Type of "a" is "ClassSelector[MyClass]"
  /home/shh/projects/holoviz/repos/param/mre_minimal2.py:40:13 - error: "assert_type" mismatch: expected "ClassSelector[MyClass | None]" but received "ClassSelector[T@ClassSelector | None]" (reportAssertTypeFailure)
  /home/shh/projects/holoviz/repos/param/mre_minimal2.py:41:13 - information: Type of "b" is "ClassSelector[T@ClassSelector | None]"
1 error, 2 warnings, 2 informations
+ pyrefly check mre_minimal2.py
ERROR `__init__` method self type cannot reference class type parameter `T` [invalid-annotation]
  --> mre_minimal2.py:13:13
   |
13 |         def __init__(
   |             ^^^^^^^^
   |
ERROR `__init__` method self type cannot reference class type parameter `T` [invalid-annotation]
  --> mre_minimal2.py:21:13
   |
21 |         def __init__(
   |             ^^^^^^^^
   |
 INFO revealed type: ClassSelector[MyClass] [reveal-type]
  --> mre_minimal2.py:37:12
   |
37 | reveal_type(a)
   |            ---
   |
ERROR `MyClass` is not assignable to upper bound `object` of type variable `T` [bad-specialization]
  --> mre_minimal2.py:39:18
   |
39 | b = ClassSelector(class_=MyClass)
   |                  ^^^^^^^^^^^^^^^^
   |
ERROR Argument `type[MyClass]` is not assignable to parameter `class_` with type `type[Unknown | None]` in function `ClassSelector.__init__` [bad-argument-type]
  --> mre_minimal2.py:39:26
   |
39 | b = ClassSelector(class_=MyClass)
   |                          ^^^^^^^
   |
ERROR assert_type(ClassSelector[Unknown | None], ClassSelector[MyClass | None]) failed [assert-type]
  --> mre_minimal2.py:40:12
   |
40 | assert_type(b, "ClassSelector[MyClass | None]")
   |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
 INFO revealed type: ClassSelector[Unknown | None] [reveal-type]
  --> mre_minimal2.py:41:12
   |
41 | reveal_type(b)
   |            ---
   |
 INFO 5 errors
+ mypy mre_minimal2.py
mre_minimal2.py:37: note: Revealed type is "mre_minimal2.ClassSelector[mre_minimal2.MyClass]"
mre_minimal2.py:41: note: Revealed type is "mre_minimal2.ClassSelector[mre_minimal2.MyClass | None]"
Success: no issues found in 1 source file
+ ty check mre_minimal2.py
info[revealed-type]: Revealed type
  --> mre_minimal2.py:37:13
   |
35 | a = ClassSelector(default=MyClass(), class_=MyClass)
36 | assert_type(a, "ClassSelector[MyClass]")
37 | reveal_type(a)
   |             ^ `ClassSelector[MyClass]`
38 |
39 | b = ClassSelector(class_=MyClass)
   |

info[revealed-type]: Revealed type
  --> mre_minimal2.py:41:13
   |
39 | b = ClassSelector(class_=MyClass)
40 | assert_type(b, "ClassSelector[MyClass | None]")
41 | reveal_type(b)
   |             ^ `ClassSelector[None | MyClass]`
   |

Found 2 diagnostics

Metadata

Metadata

Assignees

Labels

genericsBugs or features relating to ty's generics implementation

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions