dbader.org

Abstract Base Classes in Python

Abstract base classes (ABCs) enforce that derived classes implement particular methods from the base class. In this article you’ll learn about the benefits of abstract base classes and how to define them with Python’s built-in “abc” module.

What the ABC?

A while ago we had a discussion at work about which pattern to use for implementing a maintainable class hierarchy in Python. More specifically, we wanted to define a simple class hierarchy for a service backend in the most programmer-friendly and maintainable way.

In our case there was a BaseService that defines a common interface and several concrete implementations that do different things but all provide the same interface (MockService, RealService, and so on). To make this relationship explicit the concrete implementations all subclass BaseService.

To be as maintainable and programmer-friendly as possible we wanted to make sure that:

  • instantiating the base class is impossible; and
  • forgetting to implement interface methods in one of the subclasses raises an error as early as possible.

Why to use Python’s abc module

The above design problem is pretty common. To enforce that a derived class implements a number of methods from the base class we can use something like this Python idiom.

class Base:
    def foo(self):
        raise NotImplementedError()

    def bar(self):
        raise NotImplementedError()
class Concrete(Base):
    def foo(self):
        return "foo() called"

    # Oh no, we forgot to override `bar()`.
    # def bar(self):
    #     return "bar() called"

So, what do we get from this? Instantiating and using Concrete works as expected and calling methods on an an instance of Base correctly raises NotImplementedError.

>>> c = Concrete()
>>> c.foo()
'foo() called'
>>> c.bar()
NotImplementedError
>>> b = Base()
>>> b.foo()
NotImplementedError

The downside here is that we can still:

  • instantiate Base just fine without getting an error; and
  • provide incomplete subclasses — instantiating Concrete will not raise an error until we call the missing method bar().

With Python’s abc module (available since Python 2.6) we can do much better.

from abc import ABCMeta, abstractmethod

class Base(metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass
class Concrete(Base):
    def foo(self):
        pass

    # We forget to declare `bar`() again.

This still behaves as expected and creates the correct class hierarchy.

assert issubclass(Concrete, Base)

Yet, we do get something awesome here. Subclasses of Base raise a TypeError at instantiation time whenever we forget to implement any abstract methods! The raised exception tells us which method or methods we’re missing.

>>> c = Concrete()
TypeError:
"Can't instantiate abstract class Concrete with abstract methods bar"

Without abc we’d only get a NotImplementedError if a missing method is actually called. Being notified about missing methods at instantiation time is a great advantage. It makes it harder for everyone to write invalid subclasses. This might not be a big deal writing new code, but a few weeks or months down the line I promise it’ll be helpful.

This pattern is not a full replacement for static-typing, of course. But in it’s particular use case it adds a lot of safety to Python’s duck-typing. Additionally, using abc states your intent more clearly. Therefore I encourage you to read the abc module docs and to apply this pattern in similar situations.