'enforcement for abstract properties in python3

I have this abstract class

class Kuku(ABC): 
    def __init__(self): 
        self.a = 4 
    @property 
    @abstractmethod 
    def kaka(self): 
        pass 

kaka is an abstract property, So I would expect python to enforce it being a property in inheritors, But it allows me to create:

class KukuChild(Kuku): 
     def kaka(self): 
         return 3

and KukuChild().kaka() returns 3 as if it's not a property. Is this intentional? Pycharm doesn't enforce this either, so why even add the property decorator in the abstract class?



Solution 1:[1]

I've come across this problem myself, after looking into the options of how to enforce such behaviour I came up with the idea of implementing a class that has that type checking.

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

T = TypeVar('T')


class AbstractClassVar(Generic[T]):
    pass


class Abstract(abc.ABC):
    """Inherit this class to:
        1. Enforce type checking for abstract properties.
        2. Check that abstract class members (aka. `AbstractClassVar`) are implemented.
    """

    def __init_subclass__(cls) -> None:

        def get_abstract_properties(cls) -> Set[str]:
            """Gets a class's abstract properties"""
            abstract_properties = set()
            if cls is Abstract:
                return abstract_properties
            for base_cls in cls.__bases__:
                abstract_properties.update(get_abstract_properties(base_cls))
            abstract_properties.update(
                {abstract_property[0] for abstract_property in
                 inspect.getmembers(cls, lambda a: getattr(a, "__isabstractmethod__", False) and type(a) == property)})
            return abstract_properties

        def get_non_property_members(cls) -> Set[str]:
            """Gets a class's non property members"""
            return {member[0] for member in inspect.getmembers(cls, lambda a: type(a) != property)}

        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

        cls_abstract_properties = get_abstract_properties(cls)
        cls_non_property_members = get_non_property_members(cls)
        # Type checking for abstract properties
        if cls_abstract_properties:
            for member in cls_non_property_members:
                if member in cls_abstract_properties:
                    raise TypeError(f"Wrong class implementation {cls.__name__} " +
                                    f"with abstract property {member}")
        # 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]

    @property
    @abc.abstractmethod
    def a(self):
        ...

class UpperFoo(Foo):
    # Everything should be implemented as intended or else...
    ...
  1. Not implementing the abstract property a or implementing it as anything but a property (type) will result in a TypeError
  2. The trick with the class member is quite different and uses a more complex approach with annotations, but the result is almost the same. Not Implementing the abstract class member foo_member will result in a NotImplementedError.

Solution 2:[2]

You are overriding the kaka property in the child class. You must also use @property to decorate the overridden methods in the child:

from abc import ABC, abstractmethod

class Kuku(ABC): 
    def __init__(self): 
        self.a = 4 
    @property 
    @abstractmethod 
    def kaka(self): 
        pass 

class KukuChild(Kuku): 
    @property
    def kaka(self): 
         return 3

KukuChild().kaka    # <-- use as an attribute, not a method call.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Reblochon Masque