Inheritance in Python with Abstract Base Class(ABC)

E.Y.
5 min readJan 13, 2021

--

Photo by freestocks on Unsplash

As a strongly typed dynamic language, python has no enforcement of interface like with static typed language such as Java. While it does introduce type annotations, but this can still be challenging when constructing more complex object such as a class for subclassing.

Luckily, protocol was introduced. It is similar to interfaces in that they define a collection of methods/attributes on an object and force the implementation to conform with the definition. But it is informal, meaning it is known as an accepted truth or defined in documentation but not strictly in code.

Protocols are widely supported and used especially in Python’s builtin classes.

For example, the popular iteration protocol or the sequence protocol . If you want to implement a certain protocol without conforming to its requirement, you might stumble into an error, like below with the desired sized protocol :

class Member:
def __init__(self, members):
self.members = members
m = Member([1, 2, 3])
m.members # [1, 2, 3]
len(m) # TypeError: object of type 'Member' has no len()

You can see above error due to the fact that there is no len method implemented on Member class. In oder for it to work, we need to adapt:

class Member:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
m = Member([1, 2, 3])
m.members # [1, 2, 3]
len(m) # 3

Well, this is a bit annoying, as we don’t want to have it every time raising an error before we realise certain methods are missing/do not conform. Also, it’s just very difficult to manage inheritance relationship without proper subclassing anyway. For example, a Member and SupermarketStock class can both can len(). But they are not the same thing even if they implement the same methods. And that’s where Abstract Base Classes(ABC) come to rescue.

Abstract Base Classes (ABCs)

Abstract base classes are a form of interface checking more strict than protocols. They are classes that contain abstract methods, which are methods declared but without implementation. ABCs are blueprint, cannot be instantiated, and require subclasses to provide implementations for the abstract methods.

In Python, we use the module ABC. ABC works by

  • defining an abstract base class, and ,
  • use concrete class implementing an abstract class either by
  • — register the class with the abc or,
  • — subclass directly from the abc

Let’s see an example first.

Subclassing

from abc import ABC, abstractmethodclass Flour(ABC):
@abstractmethod
def make_bread(self):
pass
class Toast(Flour):
pass
x = Toast()
========================
Traceback (most recent call last):
File "main.py", line 11, in <module>
x = Toast()
TypeError: Can't instantiate abstract class Toast with abstract methods make_bread

You can see we can’t implement Toast class without implementing the make_bread method.

from abc import ABC, abstractmethodclass Flour(ABC):
@abstractmethod
def make_bread(self):
pass
class Toast(Flour):
def make_bread(self):
print ("this is a delicious toast")
x = Toast()
x.make_bread()
========================
this is a delicious toast

Of course, even it’s not compulsory, you can add concrete implementation in the abc class, noting that it will be required to override in concrete class. This is a good way for subclasses to provide a custom logic.

from abc import ABC, abstractmethodclass Flour(ABC):
@abstractmethod
def make_bread(self):
print ("this is base bread")
class Toast(Flour):
def make_bread(self):
super().make_bread()
print ("this is a delicious toast")
x = Toast()
x.make_bread()
========================
this is base bread
this is a delicious toast

ABC Metaclass, the old and new

Sometimes you may see another kind of using ABC class with ABCMeta :

from abc import ABCMeta, abstractmethodclass Flour(metaclass=ABCMeta):
# the same as:
# __metaclass__ = ABCMeta
@abstractmethod
def make_bread(self):
pass
class Toast(Flour):
pass
x = Toast()
========================
Traceback (most recent call last):
File "main.py", line 11, in <module>
x = Toast()
TypeError: Can't instantiate abstract class Toast with abstract methods make_bread

So what’s the difference?

New class ABC has ABCMeta as its meta class. Using ABC as a base class has essentially the same effect as specifying metaclass=abc.ABCMeta, but is simpler to type and easier to read.
Update 3.4

So it’s the same. But since inheritance is more commonplace and more easily understood than __metaclass__, the abc module would benefit from a simple helper class:

 class Bread(metaclass=ABCMeta):
pass
# From a user’s point-of-view, writing an abstract base call becomes
# simpler and clearer:
from abc import ABC
class Bread(ABC):
pass

Registering

The other way of using ABC is to register the concrete class thanks to the register method that can be invoked by its instance from the metaClass.

from abc import ABC, abstractmethodclass Flour(ABC):  @abstractmethod
def make_bread(self):
pass
class Toast():
pass
Flour.register(Toast)x = Toast()print(issubclass(Toast, Flour))
print(isinstance(x, Flour))
========================
True
True

But you can see the problematic as it won’t force the implementation of abstractmethod defined on the ABC class.

That’s why it’s called “register subclass as a virtual subclass” by this way.

You may wonder, what is the use when we have class inheritance that enforce stricter checks already?

Per document, we can register unrelated concrete classes (even built-in classes) and unrelated ABCs as “virtual subclasses” — these and their descendants will be considered subclasses of the registering ABC by the built-in issubclass() function, but the registering ABC won’t show up in their MRO (Method Resolution Order) nor will method implementations defined by the registering ABC be callable (not even via super()).

So you see, virtual classes are used for instance when we want to make a class from a third-party package to be a subclass of our own abstract class. Since we can’t simply change its interface, we just register it and the intepreter will know this becomes a subclass of our own.

Note that we can simplify the register by using a decorator:

@Flour.register
class Toast:
pass
t = Toast()>>> issubclass(Toast, Flour)
True
>>> isinstance(t, Flour)
True
>>>

__subclasshook__

This method is one step further to register , as it defines whether the registered subclass is considered a subclass of this ABC. So you can customise the behaviour of issubclass without the need to call register() on every subclass. The important bit here is that it’s defined as classmethod on the class and it's called by abc.ABC.__subclasscheck__. So you can only use it if you're dealing with classes that have an ABCMeta metaclass.

It returns the following value:

  • True, the subclass is considered a subclass of this ABC;
  • False, the subclass is not considered a subclass of this ABC;
  • NotImplemented, the subclass check is continued with the normal flow.
from abc import ABCMeta

class Len(metaclass=ABCMeta):
@abstractmethod
def __iter__(self):
while False:
yield None
@classmethod
def __subclasshook__(cls, C):
if cls is Len:
if any("__len__" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented

class A(object):
pass

class B(object):
def __len__(self):
return 0

issubclass(A, Len) # False
issubclass(B, Len) # True

So this way we can force any subclass to have certain method.

__abstractmethods__ and @abstractmethod

__abstractmethods__ is a descriptor to support ABC; it wraps a slot that is empty by default (so the descriptor raises an attribute error). Most of all, it is an implementation detail of how abstract methods are handled.

A class that has a metaclass derived from ABCMeta cannot be instantiated unless all of its abstract methods and properties are overridden. The abstract methods can be called using any of the normal ‘super’ call mechanisms.

Note that the abstractmethod() only affects subclasses derived using regular inheritance; “virtual subclasses” registered with register() method are not affected.

When abstractmethod() is applied in combination with other method descriptors, it should be applied as the innermost decorator:

class C(ABC):
@property
@abstractmethod
def my_abstract_property(self):
...

That’s so much of it today!

Happy Reading!

--

--