I think this is reasonable in most cases. But there are some situations where covariant inheritance is perfectly sound, for example:
@dataclass(frozen=True)
class Base[T: complex]:
x: T
def method(self) -> T:
return self.x
# def __replace__[T2: complex](self, x: T2) -> Base[T2]: ...
class Child1[T: complex](Base[T]):
def method(self) -> T:
return self.x + 1
# @override
# def __replace__[T2: complex](self, x: T2) -> Child1[T2]: ...
class Child2(Base[complex]):
def method(self) -> T:
return self.x + 1
# @override
# def __replace__(self, x: complex) -> Child2: ...
If we assume that no other subclasses exist (i.e. they’re @sealed), then T@Base and T@Child1 can be covariant without violating LSP.