Search code examples
pythonassertabc

How to incorporate type checking in an abstract base class in Python


When I define a class, I like to include type checking (using assert) of the input variables. I am now defining a 'specialized' class Rule which inherits from an abstract base class (ABC) BaseRule, similar to the following:

import abc

class BaseRule(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def resources(self):
        pass


class Rule(BaseRule):
    def __init__(self, resources):
        assert all(isinstance(resource, Resource) for resource in resources)    # type checking
        self._resources = resources

    @property
    def resources(self):
        return self._resources


class Resource(object):
    def __init__(self, domain):
        self.domain = domain


if __name__ == "__main__":
    resources = [Resource("facebook.com")]
    rule = Rule(resources)

The assert statement in the __init__ function of the Rule class ensures that the resources input is a list (or other iterable) of Resource objects. However, this would also be the case for other classes which inherit from BaseRule, so I would like to incorporate this assertion in the abstractproperty somehow. How might I go about this?


Solution

  • Make your base class have a non-abstract property that calls separate abstract getter and setter methods. The property can do the validation you want before calling the setter. Other code (such as the __init__ method of a derived class) that wants to trigger the validation can do so by doing its assignment via the property:

    class BaseRule(object):
        __metaclass__ = abc.ABCMeta
    
        @property
        def resources(self): # this property isn't abstract and shouldn't be overridden
            return self._get_resources()
    
        @resources.setter
        def resources(self, value):
            assert all(isinstance(resource, Resources) for resource in value)
            self._set_resources(value)
    
        @abstractmethod
        def _get_resources(self):   # these methods should be, instead
            pass
    
        @abstractmethod
        def _set_resources(self, value):
            pass
    
    class Rule(BaseRule):
        def __init__(self, resources):
            self.resources = resources # assign via the property to get type-checking!
    
        def _get_resources(self):
            return self._resources
    
        def _set_resources(self, value):
            self._resources = value
    

    You might even consider moving the __init__ method from Rule into the BaseRule class, since it doesn't need any knowledge about Rule's concrete implementation.