Force an abstract class attribute to be implemented by concrete class

Question:

Considering this abstract class and a class implementing it:

from abc import ABC

class FooBase(ABC):
    foo: str
    bar: str
    baz: int

    def __init__(self):
        self.bar = "bar"
        self.baz = "baz"

class Foo(FooBase):
    foo: str = "hello"

The idea here is that a Foo class that implements FooBase would be required to specify the value of the foo attribute, but the other attributes (bar and baz) would not need to be overwritten, as they’re already handle by a method provided by the abstract class.

From a MyPy type-checking perspective, is it possible to force Foo to declare the attribute foo and raise a type-checking error otherwise?

EDIT:

The rationale is that FooBase is part of a library, and the client code should be prevented from implementing it without specifying a value for foo. For bar and baz however, these are entirely managed by the library and the client doesn’t care about them.

Asked By: Jivan

||

Answers:

This is a partial answer. You can use

class FooBase(ABC):
    @property
    @classmethod
    @abstractmethod
    def foo(cls) -> str:
        ...

class Foo(FooBase):
    foo = "hi"

def go(f: FooBase) -> str:
    return f.foo

It’s only partial because you’ll only get a mypy error if you try to instantiate Foo without an initialized foo, like

class Foo(FooBase):
    ...

Foo()  # error: Cannot instantiate abstract class "Foo" with abstract attribute "foo"

This is the same behaviour as when you have a simple @abstractmethod. Only when instantiating it is the error raised. This is expected because Foo might not be intended as a concrete class, and may itself be subclassed. You can mitigate this somewhat by stating it is a concrete class with typing.final. The following will raise an error on the class itself.

@final
class Foo(FooBase):  # error: Final class __main__.Foo has abstract attributes "foo"
   ...
Answered By: joel

Rather than relying on the user to set an attribute inside of the class body, you can instead mandate a value using __init_subclass__ as proposed by PEP 487. Additionally, you should use typing.ClassVar for class variables, otherwise it’ll mix with instance variables.

from typing import Any, ClassVar


class FooBase:
    foo: ClassVar[str]
    
    def __init_subclass__(cls, /, *, foo: str, **kwargs: Any) -> None:
        cls.foo = foo
        return super().__init_subclass__(**kwargs)


class Foo(FooBase, foo="hello"):
    pass

This syntax is clean for the user, especially when you need something more complex than setting a class variable like

class Database(DB, user=..., password=...):
    pass

which could get setup to create a Database.connection class variable.

The one downside with this approach is that further subclasses will need to continue supplying the class parameters, which can usually be fixed by implementing your own __init_subclass__:

class MainDatabase(DB, user=..., password=...):

    def __init_subclass__(cls, /, reconnect: bool = False, **kwargs: Any) -> None:
        # Create a new connection.
        if reconnect:
            kwargs.setdefault("user", cls.user)
            kwargs.setdefault("password", cls.password)
            return super().__init_subclass__(**kwargs)

        # Re-use the current connection.
        else:
            return super(DB, cls).__init_subclass__(**kwargs)


class SubDatabase(Database, reconnect=True, user=..., password=...):
    pass

Good linters should be able to recognize this type of pattern, producing errors on class Foo(FooBase): without producing errors on class SubDatabse(Database, reconnect=True):.

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.