Most Pythonic way to declare an abstract class property

Viewed 14020

Assume you're writing an abstract class and one or more of its non-abstract class methods require the concrete class to have a specific class attribute; e.g., if instances of each concrete class can be constructed by matching against a different regular expression, you might want to give your ABC the following:

@classmethod
def parse(cls, s):
    m = re.fullmatch(cls.PATTERN, s)
    if not m:
        raise ValueError(s)
    return cls(**m.groupdict())

(Maybe this could be better implemented with a custom metaclass, but try to ignore that for the sake of the example.)

Now, because overriding of abstract methods & properties is checked at instance creation time, not subclass creation time, trying to use abc.abstractmethod to ensure concrete classes have PATTERN attributes won't work — but surely there should be something there to tell anyone looking at your code "I didn't forget to define PATTERN on the ABC; the concrete classes are supposed to define their own." The question is: Which something is the most Pythonic?

  1. Pile of decorators

    @property
    @abc.abstractmethod
    def PATTERN(self):
        pass
    

    (Assume Python 3.4 or higher, by the way.) This can be very misleading to readers, as it implies that PATTERN should be an instance property instead of a class attribute.

  2. Tower of decorators

    @property
    @classmethod
    @abc.abstractmethod
    def PATTERN(cls):
        pass
    

    This can be very confusing to readers, as @property and @classmethod normally can't be combined; they only work together here (for a given value of "work") because the method is ignored once it's overridden.

  3. Dummy value

    PATTERN = ''
    

    If a concrete class fails to define its own PATTERN, parse will only accept empty input. This option isn't widely applicable, as not all use cases will have an appropriate dummy value.

  4. Error-inducing dummy value

    PATTERN = None
    

    If a concrete class fails to define its own PATTERN, parse will raise an error, and the programmer gets what they deserve.

  5. Do nothing. Basically a more hardcore variant of #4. There can be a note in the ABC's docstring somewhere, but the ABC itself shouldn't have anything in the way of a PATTERN attribute.

  6. Other???

3 Answers

I've been searching for something like this for quite a while, until yesterday I decided to dive into it. I like @SethMMorton's reply a lot, however 2 things are missing: allow a an abstract class to have a subclass that is abstract itself, and play nice with typehints and static typing tools such as mypy (which makes sense, since back in 2017 these were hardly a thing).

I started to set out to write a reply here with my own solution, however I realised I needed lots of tests and documentation, so I made it a proper python module: abstractcp.

Use (as of version 0.9.5):

class Parser(acp.Abstract):
    PATTERN: str = acp.abstract_class_property(str)

    @classmethod
    def parse(cls, s):
        m = re.fullmatch(cls.PATTERN, s)
        if not m:
            raise ValueError(s)
        return cls(**m.groupdict())

class FooBarParser(Parser):
    PATTERN = r"foo\s+bar"

    def __init__(...): ...

class SpamParser(Parser):
    PATTERN = r"(spam)+eggs"

    def __init__(...): ...

See for full use the page on pypi or github.

Alternative Answer

Using dedicated class to annotate class variables

import abc
from typing import Generic, Set, TypeVar, get_type_hints

T = TypeVar('T')


class AbstractClassVar(Generic[T]):
    pass


class Abstract(abc.ABC):
    def __init_subclass__(cls) -> None:

        def get_abstract_members(cls) -> Set[str]:
            """Gets a class's abstract members"""
            abstract_members = set()
            if cls is Abstract:
                return abstract_members
            for base_cls in cls.__bases__:
                abstract_members.update(get_abstract_members(base_cls))
            for (member_name, annotation) in get_type_hints(cls).items():
                if getattr(annotation, '__origin__', None) is AbstractClassVar:
                    abstract_members.add(member_name)
            return abstract_members

        # Implementation checking for abstract class members
        if Abstract not in cls.__bases__:
            for cls_member in get_abstract_members(cls):
                if not hasattr(cls, cls_member):
                    raise NotImplementedError(f"Wrong class implementation {cls.__name__} " +
                                              f"with abstract class variable {cls_member}")
        return super().__init_subclass__()

Usage

class Foo(Abstract):
    
    foo_member: AbstractClassVar[str]


class UpperFoo(Foo):
    # Everything should be implemented as intended or else...
    ...

Not Implementing the abstract class member foo_member will result in a NotImplementedError.

Answer was taken from my original answer to this question: enforcement for abstract properties in python3

Related