Package: abstractcp
AbstractCP -- Abstract Class PropertyThis package allows one to create classes with abstract class properties.
The initial code was inspired bythis question(and accepted answer) -- in
addition to me strugling many time with the same issue in the past.
I firtst wanted to just post this as separate answer, however since it includes quite
some python magic, and I would like to include quite some tests (and possibly update
the code in the future), I made it into a package (even though I'm not a big fan of
small packages that hardly do anything).The package is python3.6 and higher. I could consider creating a version for pre-3.6;
if you want that, please create anissue on github.TL;DR ExamplesNote: the examples usePEP-526 type hints; this is obviously optional.All examples assume the following imports:importtyingastimportabstractcpasacpNote that all typing (including theimport typing as tis optional.
In addition, for python < 3.8, theLiteraltype hint can be found intyping_extensions.classParser(acp.Abstract):PATTERN:str=acp.abstract_class_property(str)@classmethoddefparse(cls,s):m=re.fullmatch(cls.PATTERN,s)ifnotm:raiseValueError(s)returncls(**m.groupdict())classFooBarParser(Parser):PATTERN=r"foo\s+bar"def__init__(...):...classSpamParser(Parser):PATTERN=r"(spam)+eggs"def__init__(...):...Example with (more) type hints:classArray(acp.Abstract):payload:np.ndarrayDIMENSIONS:int=acp.abstract_class_property(int)def__init__(self,payload):assertlen(payload)==type(self).DIMENSIONSclassVector(Array):DIMENSIONS:t.Literal[1]=1classMatrix(Array):DIMENSIONS:t.Literal[2]=2Note that in the previous example, we actually fix the value forDIMENSIONSusingt.Literal.
This is allowed in mypy (however it may actually be a bug that it's allowed).
It would possibly feel more natural to use at.Finalhere, however mypy doesn't allow this.Note that if we forget to assign a value for DIMENSIONS, an error will occur:classOtherArray(Array):pass>TypeError:ClassOtherArraymustdefineabstractclasspropertyDIMENSIONS,orhaveAbstractasdirectparentIn some cases, however, we might indeed intend for theOtherArrayclass to be abstract as well (because we will subclass this later). If so, make OtherArray inherit from Abstract directly to fix this:classOtherArray(Array,acp.Abstract):...classOtherVector(OtherArray):DIMENSIONS=1IntroductionI quite often find myself in a situation where I want to store some configuration in a class-variable, so that I can get different behaviour in different subclasses.
Quite often this starts with a top-level base class that has the methods, but without a reasonable value to use in the configuration.
In addition, I want to make sure that I don't accidentally forget to set this configuration for some child class -- exactly the behaviour that one would expect from abstract classes.
However Python doesn't have a standard way to define abstract class variables (or class constants).
The search for a solution initially led me tothis question-- the accepted answer works well, as long as you accept that each subclass of the parent must be non-abstract.
In addition, it would not play nice at all with type-hinting and tools likemypy.So I decided to write something myself -- it started as a small StackOverflow answer, however since I felt lots of tests and docs would be required, better make it a proper module.Design ConsiderationsI had some clear requirements in mind when writing this package:Pythonic syntaxWorks well withPEP-526 style type hintsand static type checkers (if possible without any# type: ignorein either this code, and the code using this module).No runtime slowdowns (i.e.: all the work gets done at setup-time)Useful error messages -- stuff needs to be explicit, no silent failures.No need to define all abstract class properties directly in the first child -- so an abstract class can have abstract children.InstallationThe package is a 100% python package. Installation is as simple aspipinstallabstractcpUseThe system consists of 2 elements: TheAbstractbase class.
Each class that is abstract (i.e. that has abstract class properties -- this is completely independent of the ways to make a class abstract inabc) must inheritdirectlyfromAbstract, meaning thatAbstractshould be a direct parent. This is done so that it's explicit which classes are abstract (and hence, we can throw an error if a class is abstract and does not inheritdirectlyfromAbstract).The second part of the system is the_AbstractClassPropertyclass.
Every abstract class property gets assigned an_AbstractClassProperty()instance, through theacp.abstract_class_property(...)method. Note that this method has typehints to  return the exact class that you provide, so from a type checker point of view,acp.abstract_class_property(int)is identical to3(or4, or any otherintinstance). This means that we can be more flexible here, for instance doingacp.abstract_class_property(t.Dict[str, int]), however note thatacp.abstract_class_property(t.Mapping[str, int])does not work, since mypy wants a concrete type there.Note thatabstract_class_property()can only be assigned in classes that haveAbstractas direct parent.See the Examples section above for exact use.Update from 0.9.1Note that since 0.9.1 the syntax has changed a bit.
Rather than writing:classA(acp.Abstract):i=acp.AbstractInt()you now useclassA(acp.Abstract):i=acp.abstract_class_property(int)It results in cleaner code, and also means that we don't have to make our own classes for new types.FAQI'm gettingArgument 1 to "abstract_class_property" has incompatible type "object"; expected "Type[<nothing>]"errorsThis happens when you try to feed something that is not actually a type to abstract_class_property, for instancex = acp.abstract_class_property(t.Union[str, int])(or even, more correctly,t.Type[t.Union[str, int]]ort.Union[t.Type[str], t.Type[int]]. Alsox = acp.abstract_class_property(t.Type[Employee])will not work (sincet.Typedoes not actually make something a type; in this case usetype(Employee)instead (which would give you an abstract property that could receive some subclass of Employee).Note that the argument toabstract_class_propertyis only for readability and used in the__repr__of the_AbstractClassPropertyclass -- and for static typing. So as long as you satisfy static typing, all will be fine:T = t.TypeVar("T", int, str)

class A(t.Generic[T], acp.Abstract):
    VALUE_TYPE: t.Type[T] = acp.abstract_class_property(t.cast(t.Type[t.Type[T]], "union of int and str"))
    def to_value(self) -> T:
        ...Note the doublet.Type, since acp.abstract_class_property will remove 1 t.Type.Why am I getting warnings when I inherit a class fromacp.Asbtractbut don't define any abstract fieldsYou will get a Python warning if you run the following code:class A(acp.Abstract):
   i = 3You are defining classAto be abstract, however it has no fields withabstract_class_property.
In almost all cases this means that either you should add an abstract class property, or remove theacp.Abstractinherritance.Defining a class like this used to result in aTypeErrorin versions <= 0.9.8, but is a warning from version 0.9.9 forward.You can safely ignore the warning (if you understand what you're doing; for instance if you just commented out the abstract class property during development for a moment), or if you really want to silence the warning forever in production code, add the following code to your program:import warnings
warnings.filterwarnings("ignore", category=acp.AbstractClassWithoutAbstractPropertiesWarning)If you do this, I would appreciate if you drop me a line, since it probably means you've found a novel use for the package that I'd be happy to learn about (and possibly document).