Skip to content

Commit 68a2f6c

Browse files
[ty] Fix super() with TypeVar-annotated self and cls parameter (#22208)
## Summary This PR fixes `super()` handling when the first parameter (`self` or `cls`) is annotated with a TypeVar, like `Self`. Previously, `super()` would incorrectly resolve TypeVars to their bounds before creating the `BoundSuperType`. So if you had `self: Self` where `Self` is bounded by `Parent`, we'd process `Parent` as a `NominalInstance` and end up with `SuperOwnerKind::Instance(Parent)`. As a result: ```python class Parent: @classmethod def create(cls) -> Self: return cls() class Child(Parent): @classmethod def create(cls) -> Self: return super().create() # Error: Argument type `Self@create` does not satisfy upper bound `Parent` ``` We now track two additional variants on `SuperOwnerKind` for TypeVar owners: - `InstanceTypeVar`: for instance methods where self is a TypeVar (e.g., `self: Self`). - `ClassTypeVar`: for classmethods where `cls` is a `TypeVar` wrapped in `type[...]` (e.g., `cls: type[Self]`). Closes astral-sh/ty#2122. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
1 parent abaa735 commit 68a2f6c

6 files changed

Lines changed: 393 additions & 99 deletions

File tree

crates/ty_python_semantic/resources/mdtest/annotations/self.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,51 @@ reveal_type(GenericCircle[int].bar()) # revealed: GenericCircle[int]
359359
reveal_type(GenericCircle.baz(1)) # revealed: GenericShape[Literal[1]]
360360
```
361361

362+
### Calling `super()` in overridden methods with `Self` return type
363+
364+
This is a regression test for <https://github.com/astral-sh/ty/issues/2122>.
365+
366+
When a child class overrides a parent method with a `Self` return type and calls `super().method()`,
367+
the return type should be the child's `Self` type variable, not the concrete child class type.
368+
369+
```py
370+
from typing import Self
371+
372+
class Parent:
373+
def copy(self) -> Self:
374+
return self
375+
376+
class Child(Parent):
377+
def copy(self) -> Self:
378+
result = super().copy()
379+
reveal_type(result) # revealed: Self@copy
380+
return result
381+
382+
# When called on concrete types, Self is substituted correctly.
383+
reveal_type(Child().copy()) # revealed: Child
384+
```
385+
386+
The same applies to classmethods with `Self` return types:
387+
388+
```py
389+
from typing import Self
390+
391+
class Parent:
392+
@classmethod
393+
def create(cls) -> Self:
394+
return cls()
395+
396+
class Child(Parent):
397+
@classmethod
398+
def create(cls) -> Self:
399+
result = super().create()
400+
reveal_type(result) # revealed: Self@create
401+
return result
402+
403+
# When called on concrete types, Self is substituted correctly.
404+
reveal_type(Child.create()) # revealed: Child
405+
```
406+
362407
## Attributes
363408

364409
TODO: The use of `Self` to annotate the `next_node` attribute should be

crates/ty_python_semantic/resources/mdtest/class/super.md

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,13 @@ class A:
168168

169169
class B(A):
170170
def __init__(self, a: int):
171-
reveal_type(super()) # revealed: <super: <class 'B'>, B>
171+
reveal_type(super()) # revealed: <super: <class 'B'>, Self@__init__>
172172
reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
173173
super().__init__(a)
174174

175175
@classmethod
176176
def f(cls):
177-
reveal_type(super()) # revealed: <super: <class 'B'>, <class 'B'>>
177+
reveal_type(super()) # revealed: <super: <class 'B'>, type[Self@f]>
178178
super().f()
179179

180180
super(B, B(42)).__init__(42)
@@ -229,16 +229,16 @@ class Foo[T]:
229229
reveal_type(super())
230230

231231
def method4(self: Self):
232-
# revealed: <super: <class 'Foo'>, Foo[T@Foo]>
232+
# revealed: <super: <class 'Foo'>, Self@method4>
233233
reveal_type(super())
234234

235235
def method5[S: Foo[int]](self: S, other: S) -> S:
236-
# revealed: <super: <class 'Foo'>, Foo[int]>
236+
# revealed: <super: <class 'Foo'>, S@method5>
237237
reveal_type(super())
238238
return self
239239

240240
def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
241-
# revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
241+
# revealed: <super: <class 'Foo'>, S@method6> | <super: <class 'Foo'>, S@method6>
242242
reveal_type(super())
243243
return self
244244

@@ -265,6 +265,19 @@ class Foo[T]:
265265
# revealed: Unknown
266266
reveal_type(super())
267267
return self
268+
# TypeVar bounded by `type[Foo]` rather than `Foo`
269+
# TODO: Should error on signature - `self` is annotated as a class type, not an instance type
270+
def method11[S: type[Foo[int]]](self: S, other: S) -> S:
271+
# Delegates to the bound to resolve the super type
272+
reveal_type(super()) # revealed: <super: <class 'Foo'>, <class 'Foo[int]'>>
273+
return self
274+
# TypeVar bounded by `type[Foo]`, used in `type[T]` position
275+
# TODO: Should error on signature - `cls` would be `type[type[Foo[int]]]`, a metaclass
276+
# Delegates to `type[Unknown]` since `type[type[Foo[int]]]` can't be constructed
277+
@classmethod
278+
def method12[S: type[Foo[int]]](cls: type[S]) -> S:
279+
reveal_type(super()) # revealed: <super: <class 'Foo'>, Unknown>
280+
raise NotImplementedError
268281

269282
type Alias = Bar
270283

@@ -359,15 +372,15 @@ from __future__ import annotations
359372

360373
class A:
361374
def test(self):
362-
reveal_type(super()) # revealed: <super: <class 'A'>, A>
375+
reveal_type(super()) # revealed: <super: <class 'A'>, Self@test>
363376

364377
class B:
365378
def test(self):
366-
reveal_type(super()) # revealed: <super: <class 'B'>, B>
379+
reveal_type(super()) # revealed: <super: <class 'B'>, Self@test>
367380

368381
class C(A.B):
369382
def test(self):
370-
reveal_type(super()) # revealed: <super: <class 'C'>, C>
383+
reveal_type(super()) # revealed: <super: <class 'C'>, Self@test>
371384

372385
def inner(t: C):
373386
reveal_type(super()) # revealed: <super: <class 'B'>, C>
@@ -645,7 +658,7 @@ class A:
645658
class B(A):
646659
def __init__(self, a: int):
647660
super().__init__(a)
648-
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
661+
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, Self@__init__>` has no attribute `a`"
649662
super().a
650663

651664
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
@@ -670,3 +683,58 @@ reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(k
670683
# error: [not-subscriptable] "Cannot subscript object of type `<super: <class 'B'>, B>` with no `__getitem__` method"
671684
super(B, B())[0]
672685
```
686+
687+
## Subclass Using Concrete Type Instead of `Self`
688+
689+
When a parent class uses `Self` in a parameter type and a subclass overrides it with a concrete
690+
type, passing that parameter to `super().__init__()` is a type error. This is because `Self` in the
691+
parent could represent a further subclass. The fix is to use `Self` consistently in the subclass.
692+
693+
```toml
694+
[environment]
695+
python-version = "3.12"
696+
```
697+
698+
```py
699+
from __future__ import annotations
700+
from collections.abc import Mapping
701+
from typing import Self
702+
703+
class Parent:
704+
def __init__(self, children: Mapping[str, Self] | None = None) -> None:
705+
self.children = children
706+
707+
class Child(Parent):
708+
def __init__(self, children: Mapping[str, Child] | None = None) -> None:
709+
# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `Mapping[str, Self@__init__] | None`, found `Mapping[str, Child] | None`"
710+
super().__init__(children)
711+
712+
# The fix is to use `Self` consistently in the subclass:
713+
714+
class Parent2:
715+
def __init__(self, children: Mapping[str, Self] | None = None) -> None:
716+
self.children = children
717+
718+
class Child2(Parent2):
719+
def __init__(self, children: Mapping[str, Self] | None = None) -> None:
720+
super().__init__(children) # OK
721+
```
722+
723+
## Super in Protocol Classes
724+
725+
Using `super()` in a class that inherits from `typing.Protocol` (similar to beartype's caching
726+
Protocol):
727+
728+
```py
729+
from typing import Protocol, Generic, TypeVar
730+
731+
_T_co = TypeVar("_T_co", covariant=True)
732+
733+
class MyProtocol(Protocol, Generic[_T_co]):
734+
def __class_getitem__(cls, item):
735+
# Accessing parent's __class_getitem__ through super()
736+
reveal_type(super()) # revealed: <super: <class 'MyProtocol'>, type[Self@__class_getitem__]>
737+
parent_method = super().__class_getitem__
738+
reveal_type(parent_method) # revealed: @Todo(super in generic class)
739+
return parent_method(item)
740+
```

crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
2222
7 |
2323
8 | class B(A):
2424
9 | def __init__(self, a: int):
25-
10 | reveal_type(super()) # revealed: <super: <class 'B'>, B>
25+
10 | reveal_type(super()) # revealed: <super: <class 'B'>, Self@__init__>
2626
11 | reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
2727
12 | super().__init__(a)
2828
13 |
2929
14 | @classmethod
3030
15 | def f(cls):
31-
16 | reveal_type(super()) # revealed: <super: <class 'B'>, <class 'B'>>
31+
16 | reveal_type(super()) # revealed: <super: <class 'B'>, type[Self@f]>
3232
17 | super().f()
3333
18 |
3434
19 | super(B, B(42)).__init__(42)
@@ -78,16 +78,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
7878
63 | reveal_type(super())
7979
64 |
8080
65 | def method4(self: Self):
81-
66 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
81+
66 | # revealed: <super: <class 'Foo'>, Self@method4>
8282
67 | reveal_type(super())
8383
68 |
8484
69 | def method5[S: Foo[int]](self: S, other: S) -> S:
85-
70 | # revealed: <super: <class 'Foo'>, Foo[int]>
85+
70 | # revealed: <super: <class 'Foo'>, S@method5>
8686
71 | reveal_type(super())
8787
72 | return self
8888
73 |
8989
74 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
90-
75 | # revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
90+
75 | # revealed: <super: <class 'Foo'>, S@method6> | <super: <class 'Foo'>, S@method6>
9191
76 | reveal_type(super())
9292
77 | return self
9393
78 |
@@ -114,35 +114,48 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
114114
99 | # revealed: Unknown
115115
100 | reveal_type(super())
116116
101 | return self
117-
102 |
118-
103 | type Alias = Bar
119-
104 |
120-
105 | class Bar:
121-
106 | def method(self: Alias):
122-
107 | # revealed: <super: <class 'Bar'>, Bar>
123-
108 | reveal_type(super())
124-
109 |
125-
110 | def pls_dont_call_me(self: Never):
126-
111 | # revealed: <super: <class 'Bar'>, Unknown>
127-
112 | reveal_type(super())
128-
113 |
129-
114 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
130-
115 | # revealed: <super: <class 'Bar'>, Bar>
131-
116 | reveal_type(super())
117+
102 | # TypeVar bounded by `type[Foo]` rather than `Foo`
118+
103 | # TODO: Should error on signature - `self` is annotated as a class type, not an instance type
119+
104 | def method11[S: type[Foo[int]]](self: S, other: S) -> S:
120+
105 | # Delegates to the bound to resolve the super type
121+
106 | reveal_type(super()) # revealed: <super: <class 'Foo'>, <class 'Foo[int]'>>
122+
107 | return self
123+
108 | # TypeVar bounded by `type[Foo]`, used in `type[T]` position
124+
109 | # TODO: Should error on signature - `cls` would be `type[type[Foo[int]]]`, a metaclass
125+
110 | # Delegates to `type[Unknown]` since `type[type[Foo[int]]]` can't be constructed
126+
111 | @classmethod
127+
112 | def method12[S: type[Foo[int]]](cls: type[S]) -> S:
128+
113 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Unknown>
129+
114 | raise NotImplementedError
130+
115 |
131+
116 | type Alias = Bar
132132
117 |
133-
118 | class P(Protocol):
134-
119 | def method(self: P):
135-
120 | # revealed: <super: <class 'P'>, P>
133+
118 | class Bar:
134+
119 | def method(self: Alias):
135+
120 | # revealed: <super: <class 'Bar'>, Bar>
136136
121 | reveal_type(super())
137137
122 |
138-
123 | class E(enum.Enum):
139-
124 | X = 1
140-
125 |
141-
126 | def method(self: E):
142-
127 | match self:
143-
128 | case E.X:
144-
129 | # revealed: <super: <class 'E'>, E>
145-
130 | reveal_type(super())
138+
123 | def pls_dont_call_me(self: Never):
139+
124 | # revealed: <super: <class 'Bar'>, Unknown>
140+
125 | reveal_type(super())
141+
126 |
142+
127 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
143+
128 | # revealed: <super: <class 'Bar'>, Bar>
144+
129 | reveal_type(super())
145+
130 |
146+
131 | class P(Protocol):
147+
132 | def method(self: P):
148+
133 | # revealed: <super: <class 'P'>, P>
149+
134 | reveal_type(super())
150+
135 |
151+
136 | class E(enum.Enum):
152+
137 | X = 1
153+
138 |
154+
139 | def method(self: E):
155+
140 | match self:
156+
141 | case E.X:
157+
142 | # revealed: <super: <class 'E'>, E>
158+
143 | reveal_type(super())
146159
```
147160

148161
# Diagnostics
@@ -205,6 +218,7 @@ error[invalid-super-argument]: `S@method10` is a type variable with an abstract/
205218
100 | reveal_type(super())
206219
| ^^^^^^^
207220
101 | return self
221+
102 | # TypeVar bounded by `type[Foo]` rather than `Foo`
208222
|
209223
info: Type variable `S` has upper bound `(...) -> str`
210224
info: rule `invalid-super-argument` is enabled by default

0 commit comments

Comments
 (0)