Previously, one the "solutions" to this type of problem was to stack @property
, @classmethod
, and @abstractmethod
together to produce an "abstract class property`.
According to CPython issue #89519, chaining descriptor decorators like @classmethod
or @staticmethod
with @property
can behave really poorly, so it has been decided that chaining decorators like this is deprecated beginning Python 3.11, and will now error with tools like mypy
.
There is an alternative solution if you really need something that behaves like an abstract class property, as explained in this comment, especially if you need a property for some expensive/delayed accessing. The trick is to supplement using @abstractmethod
with subclassing typing.Protocol
.
from typing import ClassVar, Protocol
class FooBase(Protocol):
foo: ClassVar[str]
class Foo(FooBase):
foo = "hello"
class Bar(FooBase):
pass
Foo()
Bar() # Cannot instantiate abstract class "Bar" with abstract attribute "foo"
Note that although linters can catch this type of error, it is not enforced at runtime, unlike creating a subclass of abc.ABC
which causes a runtime error if you try to instantiate a class with an abstract property.
Additionally, the above approach does not support the use of foo = Descriptor()
, similar to implementing an attribute with a @property
instead. To cover both cases, you'll need to use the following:
from typing import Any, ClassVar, Optional, Protocol, Type, TypeVar, Union
T_co = TypeVar("T_co", covariant=True)
class Attribute(Protocol[T]):
def __get__(self, instance, owner=None) -> T_co:
...
class FooBase(Protocol):
foo: ClassVar[Union[Attribute[str], str]]
class Foo(FooBase):
foo = "hello"
class Foo:
def __get__(self, instance: Any, owner: Optional[Type] = None) -> str:
return "hello"
class Bar(FooBase):
foo = Foo()
Foo()
Bar()
Both classes pass type checks and actually work at runtime as intended, although again nothing is enforced at runtime.