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
https://docs.python.org/3/library/abc.htmlABC
is stillABCMeta
, therefore inheriting fromABC
requires the usual precautions regarding metaclass usage, as multiple inheritance may lead to metaclass conflicts.
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 thematch_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 shownTypeError
, indicating thatIncompleteSearch
can’t be instantiated because it’s still “abstract” due to the unimplementedmatch_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.