Elevate Your Python: Harnessing the Power of Abstract Base Classes (ABCs)

By on 26 January 2024

Introduction

One cool object-oriented programming (OOP) technique / pattern is enforcing consistent interfaces.

In Python you can use Abstract Base Classes (ABCs) for that. 🐍

Using ABCs ensures that all subclasses implement the required methods.

This can make it easier to maintain and extend the existing code base.

Update Feb 2024: you can also leverage typing.Protocol (>= 3.8) for this, see this article.

A real world Pybites example

Check out this code example:

from abc import ABCMeta, abstractmethod

class PybitesSearch(metaclass=ABCMeta):
    @abstractmethod
    def match_content(self, search: str) -> list:
        """Implement in subclass to search Pybites content"""


class IncompleteSearch(PybitesSearch):
    pass


search = IncompleteSearch()

# TypeError: Can't instantiate abstract class IncompleteSearch
# with abstract methods match_content

Note: In the example above, we’ve used the ABCMeta metaclass directly for defining the abstract base class.

However, Python provides a more modern and succinct way to achieve the same result using the ABC class from the abc module.

Note that the type of ABC is still ABCMeta, therefore inheriting from ABC requires the usual precautions regarding metaclass usage, as multiple inheritance may lead to metaclass conflicts.

https://docs.python.org/3/library/abc.html

How does this work?

This error might be a bit confusing at first, no? 🤯

When a class is declared as an abstract class in Python, it means that the class serves as a template for other classes.

This template can define abstract methods, which are methods that are declared but not implemented. Any subclass derived from this abstract class must implement these abstract methods.

When you try to instantiate a subclass that hasn’t implemented all of the abstract methods of its parent class, Python prevents it. 🎉

This is because the subclass is still considered abstract, as it doesn’t provide concrete implementations for all the abstract methods it inherited.

In this example:

  • PybitesSearch is an abstract class because it has at least one abstract method, match_content.
  • When you create a subclass like IncompleteSearch and don’t implement the match_content method, IncompleteSearch inherits the abstract status from PybitesSearch. It’s like an incomplete blueprint.
  • As mentioned before Python enforces that you cannot create instances of abstract classes. Therefore, when you attempt to instantiate IncompleteSearch, Python raises shown TypeError, indicating that IncompleteSearch can’t be instantiated because it’s still “abstract” due to the unimplemented match_content method.

Key to this pattern is the @abstractmethod decorator. This decorator, applied to a method within an abstract base class, marks the method as one that must be overridden in any concrete subclass.

Without implementing these abstract methods, the subclass remains abstract, and Python will prevent its instantiation. It’s a clear signal to developers about the essential methods that need to be defined in any subclass.

ABCs in the wild

Abstract Base Classes are widely used in Python, both in the Standard Library as well as in third-party libraries, to provide a formal way to define interfaces. Let’s look at a few examples …

Collections Module

Iterable, Iterator, Sequence (and others): These ABCs in the collections.abc module are used to define the expected methods for iterable objects (like lists and tuples), iterators, and sequence types.

For instance, if you create a custom collection type and want it to be treated as a sequence, you can inherit from Sequence and implement __getitem__.

Here is another example if Iterable

# collections._collections_abc
class Iterable(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __iter__(self):
        while False:
            yield None
    ...


# another module
from collections.abc import Iterable

class CustomList(Iterable):
    def __init__(self, items):
        self.items = items

    def __iter__(self):
        return iter(self.items)

# CustomList must implement __iter__ adhering to the Iterable interface

io Module

The io module provides a hierarchy of ABCs for handling streaming data (like files and streams).

These ABCs can be used to enforce the implementation of standard stream methods.

from io import IOBase

class CustomTextIO(IOBase):
    def __init__(self, text):
        self.text = text
        self.index = 0

    def read(self, size=-1):
        if size == -1:
            return self.text
        else:
            data = self.text[self.index:self.index + size]
            self.index += size
            return data

file_like_object = CustomTextIO("Hello, World!")
print(file_like_object.read(5))  # Hello

# take read() method out and you'll get an error
# AttributeError: 'CustomTextIO' object has no attribute 'read'

contextlib Module

AbstractContextManager can be used to create robust and reusable context managers that adhere to the context manager protocol, ensuring the proper implementation of the required methods:

class AbstractContextManager(abc.ABC):
    """An abstract base class for context managers."""

    __class_getitem__ = classmethod(GenericAlias)

    def __enter__(self):
        """Return `self` upon entering the runtime context."""
        return self

    @abc.abstractmethod
    def __exit__(self, exc_type, exc_value, traceback):
        """Raise any exception triggered within the runtime context."""
        return None

    ...

As demonstrated before leaving off the abstract method in the subclass raises a TypeError:


from contextlib import AbstractContextManager

class chdir(AbstractContextManager):
    def __init__(self, path):
        self.path = path

ch = chdir("/tmp")

# TypeError: Can't instantiate abstract class chdir with abstract method __exit__

If you’re curious why only __exit__ is enforced and not __enter__, note that the latter is already provided with a default implementation that simply returns self, as this is often all you need. Of course subclasses can still override __enter__ if they need different behavior, but they are not forced to do so.

Flexibility

This is similar to the reason why in the PybitesSearch ABC shown earlier, I chose to make only the match_content method abstract and not get_data or show_matches.

These latter methods offer good default implementations that are sufficient for general use cases, so I didn’t require them to be implemented in every subclass.

This approach aligns with the philosophy of providing sensible defaults while allowing for flexibility.

Subclasses are free to override get_data and show_matches if specialized behavior is needed, thanks to regular inheritance.

This mix of some methods being abstract (enforcing their implementation) and others being regular (providing defaults but allowing overrides) exemplifies a flexible and powerful design pattern.

It allows for the creation of a robust and versatile framework where the essential parts are guaranteed to be implemented, while still offering customization where it’s beneficial.

When to not use ABCs

While ABCs are powerful, they come with nuances. One common pitfall is overusing them, especially when simple functions could suffice (related article: When to write classes).

ABCs are ideal for larger, more complex systems where multiple objects share a common interface. That said, Pybites Search is a relatively small project and I think they are a nice fit there.

>= 3.8 – Expanding interface enforcement with typing.Protocol

While Abstract Base Classes (ABCs) are a robust way to enforce interfaces in Python, there’s another modern approach that’s gaining traction, especially for projects where type checking plays a pivotal role.

This is where typing.Protocol comes into play. Introduced in Python 3.8, typing.Protocol allows for the definition of an interface similar to ABCs, but it enforces these interfaces at the type checking level, rather than at runtime.

I will cover this in the next article because it requires a bit more detail …

Conclusion

ABCs are a way of enforcing a contract: “If you want to be a type of PybitesSearch, you must know how to match_content.

Pybites Search has gotten quite a few subtypes since (ArticleSearch, BiteSearch, etc.) and lived happily ever after. 😄

You can look at the code here.

Please use it next time you want to look for Pybites content from your terminal. 📈

And if you see any improvements please open an issue on the repo … 🙏

Contributing to open-source projects like the Pybites search tool not only enhances your coding skills but also allows you to be part of a collaborative community.

You gain experience in real-world projects, receive feedback from fellow developers, and contribute to tools that benefit the wider community. It’s a rewarding way to learn, grow, and connect with others in the field.

What is your experience?

I encourage you to explore ABCs more in your projects if you see a good use case to enforce an interface.

When you do, or if you have already experienced their power, join our community and tell us.

Want a career as a Python Developer but not sure where to start?