Skip to content

Commit 947a03a

Browse files
committed
more review comments
1 parent da211a9 commit 947a03a

File tree

5 files changed

+68
-52
lines changed

5 files changed

+68
-52
lines changed

crates/ty/docs/rules.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_python_semantic/resources/mdtest/liskov.md

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ generally makes about types in Python:
88
99
In order for a type checker's assumptions to be sound, it is crucial for the type checker to enforce
1010
the Liskov Substitution Principle on code that it checks. In practice, this usually manifests as
11-
three checks for a type checker to perform when it checks a subclass `B` of a class `A`:
11+
several checks for a type checker to perform when it checks a subclass `B` of a class `A`:
1212

1313
1. Read-only attributes should only ever be overridden covariantly: if a property `A.p` resolves to
1414
`int` when accessed, accessing `B.p` should either resolve to `int` or a subtype of `int`.
15-
1. Method return types should only ever be overidden covariantly: if a method `A.f` returns `int`
15+
1. Method return types should only ever be overridden covariantly: if a method `A.f` returns `int`
1616
when called, calling `B.f` should also resolve to `int or a subtype of`int\`.
1717
1. Method parameters should only ever be overridden contravariantly: if a method `A.f` can be called
1818
with an argument of type `bool`, then the method `B.f` must also be callable with type `bool`
1919
(though it is permitted for the override to also accept other types)
2020
1. Mutable attributes should only ever be overridden invariantly: if a mutable attribute `A.attr`
21-
resolves to type `str`, it can only be overidden on a subclass with exactly the same type.
21+
resolves to type `str`, it can only be overridden on a subclass with exactly the same type.
2222

2323
## Method return types
2424

@@ -53,7 +53,7 @@ class Sub1(Super):
5353
def method(self, x: int, /): ... # fine
5454

5555
class Sub2(Super):
56-
def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `bool`
56+
def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `int`
5757

5858
class Sub4(Super):
5959
def method(self, x: int | str, /): ... # fine
@@ -87,7 +87,7 @@ class Sub12(Super):
8787

8888
class Sub13(Super):
8989
# Some calls permitted by the superclass are now no longer allowed
90-
# (the method can no longer be passed with exactly one argument!)
90+
# (the method can no longer be passed exactly one argument!)
9191
def method(self, x, y, /): ... # error: [invalid-method-override]
9292

9393
class Sub14(Super):
@@ -464,8 +464,6 @@ source-code definitions. There are several scenarios to consider here:
464464
subclass
465465
1. A "normal" method on a superclass is overridden by a synthesized method on a subclass
466466
1. A synthesized method on a superclass is overridden by a synthesized method on a subclass
467-
1. No methods are overridden, but the Liskov Substitution Principle is arguably violated anyway
468-
because a generated method on a superclass is incompatible with subclassing(!)
469467

470468
<!-- snapshot-diagnostics -->
471469

@@ -490,13 +488,22 @@ class Bar(Foo):
490488
class Bar2(Foo):
491489
y: str
492490

493-
# TODO: Although this class does not override any methods of `Foo`, it nonetheless
494-
# arguably violates the Liskov Substitution Principle. Instances of `Bar3` cannot be
495-
# substituted wherever an instance of `Foo` is expected, because the generated
496-
# `__lt__` method on `Foo` raises an error unless the r.h.s. and `l.h.s.` have exactly
497-
# the same `__class__` (it does not permit instances of `Foo` to be compared with
498-
# instances of subclasses of `Foo`). We could therefore consider treating all
499-
# `order=True` dataclasses as implicitly `@final` in order to enforce Liskov soundness.
491+
# TODO: Although this class does not override any methods of `Foo`, the design of the
492+
# `order=True` stdlib dataclasses feature itself arguably violates the Liskov Substitution
493+
# Principle! Instances of `Bar3` cannot be substituted wherever an instance of `Foo` is
494+
# expected, because the generated `__lt__` method on `Foo` raises an error unless the r.h.s.
495+
# and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
496+
# be compared with instances of subclasses of `Foo`).
497+
#
498+
# Many users would probably like their type checkers to alert them to cases where instances
499+
# of subclasses cannot be substituted for instances of superclasses, as this violates many
500+
# assumptions a type checker will make and makes it likely that a type checker will fail to
501+
# catch type errors elsewhere in the user's code. We could therefore consider treating all
502+
# `order=True` dataclasses as implicitly `@final` in order to enforce soundness. However,
503+
# this probably shouldn't be reported with the same error code as Liskov violations, since
504+
# the error does not stem from any method signatures written by the user. The example is
505+
# only included here for completeness.
506+
#
500507
# Note that no other type checker catches this error as of 2025-11-21.
501508
class Bar3(Foo): ...
502509

crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut…_-_Method_parameters_(d98059266bcc1e13).snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
1919
5 | def method(self, x: int, /): ... # fine
2020
6 |
2121
7 | class Sub2(Super):
22-
8 | def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `bool`
22+
8 | def method(self, x: object, /): ... # fine: `method` still accepts any argument of type `int`
2323
9 |
2424
10 | class Sub4(Super):
2525
11 | def method(self, x: int | str, /): ... # fine
@@ -53,7 +53,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
5353
39 |
5454
40 | class Sub13(Super):
5555
41 | # Some calls permitted by the superclass are now no longer allowed
56-
42 | # (the method can no longer be passed with exactly one argument!)
56+
42 | # (the method can no longer be passed exactly one argument!)
5757
43 | def method(self, x, y, /): ... # error: [invalid-method-override]
5858
44 |
5959
45 | class Sub14(Super):
@@ -131,7 +131,7 @@ error[invalid-method-override]: Invalid override of method `method`
131131
--> src/mdtest_snippet.pyi:43:9
132132
|
133133
41 | # Some calls permitted by the superclass are now no longer allowed
134-
42 | # (the method can no longer be passed with exactly one argument!)
134+
42 | # (the method can no longer be passed exactly one argument!)
135135
43 | def method(self, x, y, /): ... # error: [invalid-method-override]
136136
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Super.method`
137137
44 |

crates/ty_python_semantic/resources/mdtest/snapshots/liskov.md_-_The_Liskov_Substitut…_-_Synthesized_methods_(9e6e6c7368530460).snap

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,40 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
3232
18 | class Bar2(Foo):
3333
19 | y: str
3434
20 |
35-
21 | # TODO: Although this class does not override any methods of `Foo`, it nonetheless
36-
22 | # arguably violates the Liskov Substitution Principle. Instances of `Bar3` cannot be
37-
23 | # substituted wherever an instance of `Foo` is expected, because the generated
38-
24 | # `__lt__` method on `Foo` raises an error unless the r.h.s. and `l.h.s.` have exactly
39-
25 | # the same `__class__` (it does not permit instances of `Foo` to be compared with
40-
26 | # instances of subclasses of `Foo`). We could therefore consider treating all
41-
27 | # `order=True` dataclasses as implicitly `@final` in order to enforce Liskov soundness.
42-
28 | # Note that no other type checker catches this error as of 2025-11-21.
43-
29 | class Bar3(Foo): ...
44-
30 |
45-
31 | class Eggs:
46-
32 | def __lt__(self, other: Eggs) -> bool: ...
47-
33 |
48-
34 | # TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
49-
35 | # We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
50-
36 | # diagnostic here but pyright and pyrefly do not.
51-
37 | @dataclass(order=True)
52-
38 | class Ham(Eggs):
53-
39 | x: int
54-
40 |
55-
41 | class Baz(NamedTuple):
56-
42 | x: int
57-
43 |
58-
44 | class Spam(Baz):
59-
45 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
35+
21 | # TODO: Although this class does not override any methods of `Foo`, the design of the
36+
22 | # `order=True` stdlib dataclasses feature itself arguably violates the Liskov Substitution
37+
23 | # Principle! Instances of `Bar3` cannot be substituted wherever an instance of `Foo` is
38+
24 | # expected, because the generated `__lt__` method on `Foo` raises an error unless the r.h.s.
39+
25 | # and `l.h.s.` have exactly the same `__class__` (it does not permit instances of `Foo` to
40+
26 | # be compared with instances of subclasses of `Foo`).
41+
27 | #
42+
28 | # Many users would probably like their type checkers to alert them to cases where instances
43+
29 | # of subclasses cannot be substituted for instances of superclasses, as this violates many
44+
30 | # assumptions a type checker will make and makes it likely that a type checker will fail to
45+
31 | # catch type errors elsewhere in the user's code. We could therefore consider treating all
46+
32 | # `order=True` dataclasses as implicitly `@final` in order to enforce soundness. However,
47+
33 | # this probably shouldn't be reported with the same error code as Liskov violations, since
48+
34 | # the error does not stem from any method signatures written by the user. The example is
49+
35 | # only included here for completeness.
50+
36 | #
51+
37 | # Note that no other type checker catches this error as of 2025-11-21.
52+
38 | class Bar3(Foo): ...
53+
39 |
54+
40 | class Eggs:
55+
41 | def __lt__(self, other: Eggs) -> bool: ...
56+
42 |
57+
43 | # TODO: the generated `Ham.__lt__` method here incompatibly overrides `Eggs.__lt__`.
58+
44 | # We could consider emitting a diagnostic here. As of 2025-11-21, mypy reports a
59+
45 | # diagnostic here but pyright and pyrefly do not.
60+
46 | @dataclass(order=True)
61+
47 | class Ham(Eggs):
62+
48 | x: int
63+
49 |
64+
50 | class Baz(NamedTuple):
65+
51 | x: int
66+
52 |
67+
53 | class Spam(Baz):
68+
54 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
6069
```
6170

6271
# Diagnostics
@@ -86,21 +95,21 @@ info: rule `invalid-method-override` is enabled by default
8695

8796
```
8897
error[invalid-method-override]: Invalid override of method `_asdict`
89-
--> src/mdtest_snippet.pyi:45:9
98+
--> src/mdtest_snippet.pyi:54:9
9099
|
91-
44 | class Spam(Baz):
92-
45 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
100+
53 | class Spam(Baz):
101+
54 | def _asdict(self) -> tuple[int, ...]: ... # error: [invalid-method-override]
93102
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Baz._asdict`
94103
|
95104
info: This violates the Liskov Substitution Principle
96105
info: `Baz._asdict` is a generated method created because `Baz` inherits from `typing.NamedTuple`
97-
--> src/mdtest_snippet.pyi:41:7
106+
--> src/mdtest_snippet.pyi:50:7
98107
|
99-
39 | x: int
100-
40 |
101-
41 | class Baz(NamedTuple):
108+
48 | x: int
109+
49 |
110+
50 | class Baz(NamedTuple):
102111
| ^^^^^^^^^^^^^^^ Definition of `Baz`
103-
42 | x: int
112+
51 | x: int
104113
|
105114
info: rule `invalid-method-override` is enabled by default
106115

ty.schema.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)