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
When a generic class is defined with one
TypeVar('T')but its__init__overloads use a differentTypeVar('CT')in theself-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
Expected Behavior
Actual Behavior
ty
pyright:
pyrefly
mypy
Version
ty 0.0.29
Note:
If I change
CTtoTit works for ty, but makes other type checker fails: