python abstract instance property alternative

Question:

If I have for example:

class Parent(object):
    @property
    @abc.abstractmethod
    def asdf(self) -> str:
        """ Must be implemented by child """

@dataclass
class Children(Parent):
    asdf = "1234"
    
    def some_method(self):
        self.asdf = "5678"

I get an error from mypy saying I am shadowing class attribute in some_method. This is understandable, since asdf is defined in Parent and so becomes a class attribute. But, if I do this instead:

class Parent(object):
    asdf: str

No error is generated by mypy, but then the assignment to asdf is not enforced. And also, asdf is still nonetheless a class attribute right? Are class attributes in general not meant to be overridden by methods in children? How can I make a parent class that enforces its children to have a certain instance attribute (and not class attributes)?

If I look at other languages like C#, I think these ‘class attributes’ would be kind of equivalent to properties, but there we can customize its get and set behavior. Why is it not the case with Python?

Asked By: aldo

||

Answers:

You might be misunderstanding the purpose of the @property decorator. Generally it’s used for a function that is supposed to be accessed / "feel like" a constant to outside code.

Python does not do strict type-checking of variables without significant effort. (It does constrain the types of values at runtime, so a variable’s type won’t change subtly / unexpectedly. However, it will normally allow a string to be passed / returned in any place nominally expecting an integer – or vice versa. The value which is passed at runtime will continue to be used, without conversion as its original type, and without an error being thrown – until & unless a conversion is required & not provided. For example, type hints marking a variable as a string don’t prevent assignment of a float to that variable. An error will only be thrown if there’s an explicit type check, or if you try to call a method defined for strings but not for floats – e.g. str.join().)

The grounding assumption in that is "other developers know what they’re doing as much as the current developer does", so you have to do some workarounds to get strict type enforcement of the type you’d see in C#, Java, Scala and so on. Think of the "type hints" more like documentation and help for linters and the IDE than strict type enforcement.

This point of view gives a few alternatives, depending on what you want from Children.asdf: How strict should the check be? Is the class-constant string you’ve shown what you’re looking for, or do you want the functionality normally associated with the @property decorator?

First draft, as a very non-strict constant string, very much in Python’s EAFP tradition:

class Parent:
    ASDF: str = None # Subclasses are expected to define a string for ASDF.

class Children(Parent):
    ASDF = 'MyChildrenASDF'

If you want to (somewhat) strictly enforce that behavior, you could do something like this:

class Parent:
    ASDF: str = None # Subclasses are required to define a string for ASDF.
 
    def __init__(self):
        if not isinstance(self.ASDF, str):
            # This uses the class name of Children or whatever 
            # subclasses Parent in the error message, which makes 
            # debugging easier if you have many subclasses and multiple 
            # developers.
            raise TypeError(f'{self.__class__.__name__}.ASDF must be a string.')

class Children(Parent):
    ASDF = 'MyChildrenASDF'

    def __init__(self):
        # This approach does assume the person writing the subclass
        # remembers to call super().__init__().  That's not enforced
        # automatically.
        super().__init__()

The second option is about as strict as I’d go personally, except in rare circumstances. If you need greater enforcement, you could write a unit test which loops over all Parent.__subclasses__(), and performs the check each time tests are run.

Alternately, you could define a Python metaclass. Note that metaclasses are an "advanced topic" in Python, and the general rule of thumb is "If you don’t know whether you need a metaclass, or don’t know what a metaclass is, you shouldn’t use a metaclass". Basically, metaclasses let you hack the class-definition process: You can inject class attributes which are automatically defined on the fly, throw errors if things aren’t defined, and all sorts of other wacky tricks… but it’s a deep rabbit hole and probably overkill for most use cases.

If you want something which is actually a function, but uses the @property decorator so it feels like an instance property, you could do this:

class Parent:
    @property
    def asdf(self) -> str:
         prepared = self._calculate_asdf()
         if not isinstance(prepared):
            raise TypeError(f'{self.__class__.__name__}._calculate_asdf() must return a string.')

    def _calculate_asdf(self) -> str:
         raise NotImplementedError(f'{self.__class__.__name__}._calculate_asdf() must return a string.')

class Children(Parent):
    def _calculate_asdf(self) -> str:
        # I needed a function here to show what the `@property` could
        # do. This one seemed convenient. Any function returning a
        # string would be fine.
        return self.__class__.__name__.reverse()
Answered By: Sarah Messer
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.