What I mean here is something like this although really only one decorator should be used:
from typing import subclass_compatible, not_subclass_compatible
class Base:
@subclass_compatible
@classmethod
def compat(cls) -> None:
pass
@not_subclass_compatible
@classmethod
def incompat(cls) -> None:
pass
class Sub(Base):
# type check error (not compatible with Base)
@classmethod
def compat(cls, a: str) -> None:
pass
# This is fine (does not need to be compatible)
@classmethod
def incompat(cls, a: str) -> None:
pass
def func(typ: type[Base]):
typ.compat() # fine
typ.incompat() # type check error (incompatible method)
Decorators could be used for any class method or __new__
or __init__
. Then a type checker can distinguish which methods are supposed to be compatible across subclasses. That makes it possible for the type checker to do two things:
- Enforce that the method is always compatible in subclasses.
- Assume that the method is safe to call when given a
type[Base]
.
The current situation is that type checkers assume that all class methods and __new__
and __init__
are subclass-compatible and safe to call but then do not enforce that compatibility for __new__
and __init__
leaving the soundness gap. Having an explicit way to distinguish whether these methods are intended to be compatible allows closing the gap.
There are situations besides __new__
and __init__
where I have class methods that are not intended to be compatible but type checkers do not understand this and complain. Currently the only option for this is hundreds of type: ignore
.