Enforce the Liskov Substitution Principle for non-methods
We have implemented a basic version of the Liskov Substitution Principle to cover cases where methods are incompatibly overridden in subclasses. We also need to implement separate diagnostics to cover properties incompatibly overridden in subclasses, and mutable attributes incompatibly overridden in subclasses.
The reason why these are planned as separate diagnostics is because a large amount of code in the ecosystem unsoundly overrides mutable attributes covariantly, e.g.
class A:
x: int
class B(A):
x: bool
Partly this is because mypy allowed this for years -- and still does, unless users explicitly opt into the mutable-override error code. Implementing these as separate diagnostics to our existing invalid-method-override diagnostic will therefore allow users to switch the mutable-attribute-override rule off specifically if it causes a large number of diagnostics on their code.
Sub-tasks (many of these may share a common implementation):
-
[ ] Enforce Liskov for property types: this should cause us to emit a diagnostic:
class A: @property def f(self) -> int: return 42 class B(A): @property def f(self) -> str: ❌ Superclass returns `int`, subclass returns `str`, `str` is not a subtype of `int` return "42" -
[ ] Enforce that a writable attribute cannot be overridden with a read-only property:
class A: x: int class B(A): @property def x(self) -> int: ❌ Superclass attribute is writable, subclass attribute is read-only return 42and
class A: @property def x(self) -> int: return 42 @x.setter def x(self, value: int) -> None: ... class B(A): @property def x(self) -> int: ❌ Superclass attribute is writable, subclass attribute is read-only return 42 -
[ ] Enforce Liskov for attribute types:
class A: x: int class B(A): x: bool ❌ Type of `x` attribute is invariant because it is mutable -
[ ] Enforce that a non-
Finalattribute cannot be overridden with aFinalonefrom typing import Final class A: x: int class B(A): x: Final[int] ❌ Superclass attribute is writable, subclass attribute is read-only -
[ ] Enforce that a non-
ClassVarattribute cannot be overridden with aClassVarattribute:from typing import ClassVar class A: x: int class B(A): x: ClassVar[int] ❌ Superclass attribute is writable on instances, subclass attribute is not -
[ ] Enforce that a
ClassVarattribute cannot be overridden with an implicit instance attribute, sinceClassVars can be mutated on the class object itself but implicit instance attributes cannot:from typing import ClassVar class A: x: ClassVar[int] class B(A): def __init__(self): self.x: int ❌ Superclass attribute is writable on the class object, subclass attribute is not -
[ ] Enforce that a non-
ReadOnlyTypedDictfield cannot be overridden with aReadOnlyTypedDictfield:from typing import TypedDict, ReadOnly class A(TypedDict): x: int class B(A): x: ReadOnly[int] ❌ Superclass field is writable, subclass attribute is read-only