Updated On : Jan-23,2021 Tags abstract-base-class, abc
abc - How to Create Abstract Base Classes in Python?

abc - How to Create Abstract Base Classes in Python?

Abstract base classes introduced through abc module of Python lets us enforce some kind of behavior through our classes. It lets us define abstract methods that need to be implemented by the subclass which inherits that class and this way lets us force behavior by forcing the subclass to implement those methods. For example, If we want some class to behave like an iterator then we can define an abstract class with abstract methods iter and len. This will force the subclass of this class to implement these methods hence indicating that it’s some kind of iterator. It also lets us include non-abstract concrete methods as well as abstract methods with implementation in abstract classes. We can include basic behavior in concrete methods that can be overridden by a subclass that extends it. As a part of this tutorial, we'll explain through different examples how we can use abc module to create abstract base classes in Python.

The process of creating an abstract class is very simple using abc module.

  • We need to create an abstract class. There are two ways to do it.
    • Inheriting from ABC class
    • Setting ABCMeta as metaclass of the class.
  • Declare abstract methods by annotating them with @abstractmethod annotation.

We'll now explain with simple examples how we can create abstract base classes using Python.

Example 1

Our first example is quite simple. It declares a class named MappingBase which extends ABC without anything in it. We can simply create abstract base classes this way. Python interpreter lets us create an instance of this class (which ideally it should not) because it does not have any abstract methods. We can access abstract methods by calling abstractmethods attribute on the class reference. It returns frozenset of abstract methods.

In [1]:
from abc import ABC

class MappingBase(ABC):
    pass


print("MappingBase Abstract Methods    : ", MappingBase.__abstractmethods__)

mb = MappingBase()
MappingBase Abstract Methods    :  frozenset()

Example 2

As a part of our second example, we are building on our previous example. We have created 3 abstract methods named getitem, setitem and len. We have simply declared abstract methods with signature and nothing in the body of methods (just pass statement). We have indicated this way that whichever class that implements this class should implement these three methods. These three methods indicate that this class has a dictionary-like interface that expects the value to be returned based on key and value to be set for a particular key. The len method indicates that it should return a number of (key, val) pairs present in the data structure.

We have then printed the value of abstractmethods attribute and we can see that it prints three method names. We have then tried to instantiate the class which fails due to the presence of abstract methods.

In [2]:
from abc import ABC
from abc import abstractmethod

class MappingBase(ABC):

    @abstractmethod
    def __getitem__(self, key):
        pass

    @abstractmethod
    def __setitem__(self, key, value):
        pass

    @abstractmethod
    def __len__(self):
        pass

print("MappingBase Abstract Methods    : ", MappingBase.__abstractmethods__)

mb = MappingBase()
MappingBase Abstract Methods    :  frozenset({'__len__', '__getitem__', '__setitem__'})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-1758e0d6d0e2> in <module>
     18 print("MappingBase Abstract Methods    : ", MappingBase.__abstractmethods__)
     19
---> 20 mb = MappingBase()

TypeError: Can't instantiate abstract class MappingBase with abstract methods __getitem__, __len__, __setitem__

Example 3

Our third example is exactly the same as our second example with only a minor difference in the way we create an abstract class. This time we have set metaclass parameter of class to ABCMeta to declare that the class is an abstract class. All other things are exactly the same as the previous example.

Please make a note that we need to extend ABC class or set the metaclass attribute of class as ABCMeta to declare a class as an abstract base class. If we don't do that then it won't have abstractmethods attribute and the interpreter won't throw an error message when we try to initialize it which it should ideally.

In [3]:
from abc import ABCMeta
from abc import abstractmethod

class MappingBase(metaclass=ABCMeta):

    @abstractmethod
    def __getitem__(self, key):
        pass

    @abstractmethod
    def __setitem__(self, key, value):
        pass

    @abstractmethod
    def __len__(self):
        pass

print("MappingBase Abstract Methods    : ", MappingBase.__abstractmethods__)

mb = MappingBase()
MappingBase Abstract Methods    :  frozenset({'__len__', '__getitem__', '__setitem__'})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-c92d6c75befe> in <module>
     18 print("MappingBase Abstract Methods    : ", MappingBase.__abstractmethods__)
     19
---> 20 mb = MappingBase()

TypeError: Can't instantiate abstract class MappingBase with abstract methods __getitem__, __len__, __setitem__

Example 4

Our fourth example builds on previous examples by going ahead and implementing an abstract base class. It implements all three methods that we have declared as abstract. We have declared a concrete class named Mapping which implements our MappingBase class. We have declared a dictionary in the Mapping class which will be holding data for the class. We can now treat mapping class like it’s a dictionary. We have also declared method named str and repr which returns string representation of the class.

We have also included basic implementation details in setitem abstract method in MappingBase class where we check for the type of key to be string and type of value to be an integer. We raise an error if that’s not the case. When we implemented this method in the subclass we have first called the abstract method in superclass MappingBase to check for the type of key and value. We have enforced another behavior this way that we want this type of data to be present in our data structure Mapping.

At last, we have created an instance of the Mapping class and added few key-value pairs to it to test the implementation of methods. We have also printed results to check implementation of str and repr methods.

In [4]:
from abc import ABC
from abc import abstractmethod

class MappingBase(ABC):

    @abstractmethod
    def __getitem__(self, key):
        pass

    @abstractmethod
    def __setitem__(self, key, value):
        if not isinstance(key, str):
            raise ValueError("Keys must be String Type")

        if not isinstance(value, int):
            raise ValueError("Values must be Interger Type")

    @abstractmethod
    def __len__(self):
        pass

class Mapping(MappingBase):
    def __init__(self):
        self.dictionary = {}

    def __getitem__(self, key):
        return self.dictionary[key]

    def __setitem__(self, key, value):
        super().__setitem__(key, value)
        self.dictionary[key] = value

    def __len__(self):
        return len(self.dictionary.keys())

    def __str__(self):
        obj_repr = ""

        for key,val in sorted(self.dictionary.items(), key= lambda x: x[0]):
            obj_repr += "{} : {}\n".format(key,val)

        return obj_repr

    def __repr__(self):
        obj_repr = ""

        for key,val in sorted(self.dictionary.items(), key= lambda x: x[0]):
            obj_repr += "{} : {}, ".format(key,val)

        return obj_repr

print("\nMappingBase Abstract Methods    : ", MappingBase.__abstractmethods__)
print("Mapping     Abstract Methods    : ", Mapping.__abstractmethods__)

mb = Mapping()

for key, val in zip("EFBACD",[10,100,20,400,5,50]):
    mb[key] = val

print("\nNumber of Items in Mapping : {}\n".format(len(mb)))
print("Mapping Contents : ")
print(mb)
MappingBase Abstract Methods    :  frozenset({'__len__', '__getitem__', '__setitem__'})
Mapping     Abstract Methods    :  frozenset()

Number of Items in Mapping : 6

Mapping Contents :
A : 400
B : 20
C : 5
D : 50
E : 10
F : 100

In [5]:
mb
Out[5]:
A : 400, B : 20, C : 5, D : 50, E : 10, F : 100, 

Below we are testing whether the class lets us set non-string value as key and non-integer value as value of mapping or not.

In [6]:
mb[10] = 10
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-6-3a6a05ca21f5> in <module>
----> 1 mb[10] = 10

<ipython-input-4-0fb0a95ac835> in __setitem__(self, key, value)
     28
     29     def __setitem__(self, key, value):
---> 30         super().__setitem__(key, value)
     31         self.dictionary[key] = value
     32

<ipython-input-4-0fb0a95ac835> in __setitem__(self, key, value)
     11     def __setitem__(self, key, value):
     12         if not isinstance(key, str):
---> 13             raise ValueError("Keys must be String Type")
     14
     15         if not isinstance(value, int):

ValueError: Keys must be String Type
In [7]:
try:
    mb["X"] = 2.45
except Exception as e:
    print(e)
Values must be Interger Type

Example 5

As a part of our fifth example, we have included a new example to demonstrate usage of abc. We have created new abstract class named ArithmeticOpsBase which has four abstract method named add, subtract, divide and multiply. We have included the signature of the methods as well.

In [8]:
from abc import ABC
from abc import abstractmethod

class ArithmeticOpsBase(ABC):
    @abstractmethod
    def add(self, a, b):
        pass

    @abstractmethod
    def subtract(self, a, b):
        pass

    @abstractmethod
    def divide(self, a, b):
        pass

    @abstractmethod
    def multiply(self, a, b):
        pass

Below we have created a class named ArithmeticOperations which extends our ArithmeticOpsBase class. We have then printed a list of abstract methods in both the class. We have then tried to create an instance of ArithmeticOperations which fails because it has abstract methods that are not implemented.

In [9]:
class ArithmeticOperations(ArithmeticOpsBase):
    pass

print("\nArithmeticOpsBase Abstract Methods    : ", ArithmeticOpsBase.__abstractmethods__)
print("ArithmeticOperations Abstract Methods : ", ArithmeticOperations.__abstractmethods__)

ao = ArithmeticOperations()
ArithmeticOpsBase Abstract Methods    :  frozenset({'multiply', 'add', 'subtract', 'divide'})
ArithmeticOperations Abstract Methods :  frozenset({'multiply', 'add', 'subtract', 'divide'})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-9-a3cd65402795> in <module>
      5 print("ArithmeticOperations Abstract Methods : ", ArithmeticOperations.__abstractmethods__)
      6
----> 7 ao = ArithmeticOperations()

TypeError: Can't instantiate abstract class ArithmeticOperations with abstract methods add, divide, multiply, subtract

Below we have again created a class named ArithmeticOperations which extends ArithmeticOpsBase like the last cell but this time it implements one of the methods named add. We have again printed abstract methods for both classes and we can notice that ArithmeticOperations class has now 3 methods as abstract methods excluding add method.

This hints at an important feature of the abstract base class that we need to implement all abstract methods in the base class in order to instantiate that class and use its methods. If we don't implement all methods present in the abstract base class then the class which implements it will also turn abstract class.

In [10]:
class ArithmeticOperations(ArithmeticOpsBase):
    def add(self, a, b):
        return a + b

print("\nArithmeticOpsBase Abstract Methods    : ", ArithmeticOpsBase.__abstractmethods__)
print("ArithmeticOperations Abstract Methods : ", ArithmeticOperations.__abstractmethods__)

ao = ArithmeticOperations()
ArithmeticOpsBase Abstract Methods    :  frozenset({'multiply', 'add', 'subtract', 'divide'})
ArithmeticOperations Abstract Methods :  frozenset({'multiply', 'subtract', 'divide'})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-73376d0f7c2e> in <module>
      6 print("ArithmeticOperations Abstract Methods : ", ArithmeticOperations.__abstractmethods__)
      7
----> 8 ao = ArithmeticOperations()

TypeError: Can't instantiate abstract class ArithmeticOperations with abstract methods divide, multiply, subtract

At last, we have provided an implementation of all abstract base class methods below. We have then created an instance of ArithmeticOperations and then called all implemented methods to test their implementations.

In [11]:
class ArithmeticOperations(ArithmeticOpsBase):
    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def divide(self, a, b):
        return a/b

    def multiply(self, a, b):
        return a * b

ao = ArithmeticOperations()

print("Addition       of {} & {} is {}".format(10,20, ao.add(10,20)))
print("Subtraction    of {} & {} is {}".format(10,20, ao.subtract(10,20)))
print("Division       of {} & {} is {}".format(10,20, ao.divide(10,20)))
print("Multiplication of {} & {} is {}".format(10,20, ao.multiply(10,20)))

print("\nArithmeticOpsBase Abstract Methods    : ", ArithmeticOpsBase.__abstractmethods__)
print("ArithmeticOperations Abstract Methods : ", ArithmeticOperations.__abstractmethods__)
Addition       of 10 & 20 is 30
Subtraction    of 10 & 20 is -10
Division       of 10 & 20 is 0.5
Multiplication of 10 & 20 is 200

ArithmeticOpsBase Abstract Methods    :  frozenset({'multiply', 'add', 'subtract', 'divide'})
ArithmeticOperations Abstract Methods :  frozenset()

Example 6

Our sixth example is almost the same as our fifth example with few minor changes in code. We have introduced two concrete methods (maximum() and minimum()) in our abstract base class apart from four abstract methods in our ArithmeticOpsBase class. Another major change that we have introduced is a change in the signature of add method when we implemented it in the concrete class ArithmeticOperations. We have changed the signature of add method from 2 arguments to a method with infinite arguments. It can now perform addition on as many numbers gives as input to it.

We have then created an instance of ArithmeticOperations and tested the implementation of all methods.

In [12]:
from abc import ABC
from abc import abstractmethod

class ArithmeticOpsBase(metaclass=ABCMeta):
    @abstractmethod
    def add(self, a, b):
        pass

    @abstractmethod
    def subtract(self, a, b):
        pass

    @abstractmethod
    def divide(self, a, b):
        pass

    @abstractmethod
    def multiply(self, a, b):
        pass

    def maximum(self, *args):
        return max(*args)

    def minimum(self, *args):
        return min(*args)

class ArithmeticOperations(ArithmeticOpsBase):
    def add(self, *args):
        return sum(args)

    def subtract(self, a, b):
        return a - b

    def divide(self, a, b):
        return a/b

    def multiply(self, a, b):
        return a * b

ao = ArithmeticOperations()

print("Addition       of {} & {} is {}".format(10,20, ao.add(10,20)))
print("Subtraction    of {} & {} is {}".format(10,20, ao.subtract(10,20)))
print("Division       of {} & {} is {}".format(10,20, ao.divide(10,20)))
print("Multiplication of {} & {} is {}".format(10,20, ao.multiply(10,20)))
print("Addition       of {}, {} & {} is {}".format(10,20,30, ao.add(10,20, 30)))
print("Maximum        of {}  is {}".format([50,30,10,100,120], ao.maximum([50,30,10,100,120])))
print("Minimum        of {}  is {}".format([50,30,10,100,120], ao.minimum([50,30,10,100,120])))

print("\nArithmeticOpsBase Abstract Methods    : ", ArithmeticOpsBase.__abstractmethods__)
print("ArithmeticOperations Abstract Methods : ", ArithmeticOperations.__abstractmethods__)
Addition       of 10 & 20 is 30
Subtraction    of 10 & 20 is -10
Division       of 10 & 20 is 0.5
Multiplication of 10 & 20 is 200
Addition       of 10, 20 & 30 is 60
Maximum        of [50, 30, 10, 100, 120]  is 120
Minimum        of [50, 30, 10, 100, 120]  is 10

ArithmeticOpsBase Abstract Methods    :  frozenset({'multiply', 'add', 'subtract', 'divide'})
ArithmeticOperations Abstract Methods :  frozenset()

Example 7

As a part of our seventh example, we have demonstrated how we can create static abstract methods. Our code for this example is exactly the same as our previous example with the only change that all our methods have @staticmethod annotation on all of our methods. We have also removed the first self parameter from all methods to convert them to static methods. Once we have declared the method static, we can call the method directly without creating an instance of the class. We have then tested the implementation of all methods as well.

Please make a note that when we declare a method as static it won't be given class instance as the first parameter like instance methods (the one with the self parameter as the first parameter).

In [13]:
from abc import ABC, ABCMeta
from abc import abstractmethod

class ArithmeticOpsBase(metaclass=ABCMeta):

    @staticmethod
    @abstractmethod
    def add(a, b):
        pass

    @staticmethod
    @abstractmethod
    def subtract(a, b):
        pass

    @staticmethod
    @abstractmethod
    def divide(a, b):
        pass

    @staticmethod
    @abstractmethod
    def multiply(a, b):
        pass

    @staticmethod
    def maximum(*args):
        return max(*args)

    @staticmethod
    def minimum(*args):
        return min(*args)

class ArithmeticOperations(ArithmeticOpsBase):
    @staticmethod
    def add(a, b):
        return a + b

    @staticmethod
    def subtract(a, b):
        return a - b

    @staticmethod
    def divide(a, b):
        return a/b

    @staticmethod
    def multiply(a, b):
        return a * b

print("Addition       of {} & {} is {}".format(10,20, ArithmeticOperations.add(10,20)))
print("Subtraction    of {} & {} is {}".format(10,20, ArithmeticOperations.subtract(10,20)))
print("Division       of {} & {} is {}".format(10,20, ArithmeticOperations.divide(10,20)))
print("Multiplication of {} & {} is {}".format(10,20, ArithmeticOperations.multiply(10,20)))
print("Maximum        of {}  is {}".format([50,30,10,100,120], ArithmeticOperations.maximum([50,30,10,100,120])))
print("Minimum        of {}  is {}".format([50,30,10,100,120], ArithmeticOperations.minimum([50,30,10,100,120])))

print("\nArithmeticOpsBase Abstract Methods    : ", ArithmeticOpsBase.__abstractmethods__)
print("ArithmeticOperations Abstract Methods : ", ArithmeticOperations.__abstractmethods__)
Addition       of 10 & 20 is 30
Subtraction    of 10 & 20 is -10
Division       of 10 & 20 is 0.5
Multiplication of 10 & 20 is 200
Maximum        of [50, 30, 10, 100, 120]  is 120
Minimum        of [50, 30, 10, 100, 120]  is 10

ArithmeticOpsBase Abstract Methods    :  frozenset({'multiply', 'add', 'subtract', 'divide'})
ArithmeticOperations Abstract Methods :  frozenset()

Example 8

As a part of our eighth example, we have demonstrated how we can declare abstract class methods. Our code for this example is exactly the same as the previous example, with the main change that we have replaced annotation @classmethod. We have introduced the first parameter named cls in all our methods as they are class methods. Once we have declared methods as class methods, we can access them directly from class reference without creating an instance of the class.

Please make a note that the class method has the first parameter class reference given as input, unlike the instance method which has an instance given as the first parameter.

In [14]:
from abc import ABC, ABCMeta
from abc import abstractmethod

class ArithmeticOpsBase(metaclass=ABCMeta):

    @classmethod
    @abstractmethod
    def add(cls, a, b):
        pass

    @classmethod
    @abstractmethod
    def subtract(cls, a, b):
        pass

    @classmethod
    @abstractmethod
    def divide(cls, a, b):
        pass

    @classmethod
    @abstractmethod
    def multiply(cls, a, b):
        pass

    @classmethod
    def maximum(cls, *args):
        return max(*args)

    @classmethod
    def minimum(cls, *args):
        return min(*args)

class ArithmeticOperations(ArithmeticOpsBase):
    @classmethod
    def add(cls, a, b):
        return a + b

    @classmethod
    def subtract(cls, a, b):
        return a - b

    @classmethod
    def divide(cls, a, b):
        return a/b

    @classmethod
    def multiply(cls, a, b):
        return a * b

print("Addition       of {} & {} is {}".format(10,20, ArithmeticOperations.add(10,20)))
print("Subtraction    of {} & {} is {}".format(10,20, ArithmeticOperations.subtract(10,20)))
print("Division       of {} & {} is {}".format(10,20, ArithmeticOperations.divide(10,20)))
print("Multiplication of {} & {} is {}".format(10,20, ArithmeticOperations.multiply(10,20)))
print("Maximum        of {}  is {}".format([50,30,10,100,120], ArithmeticOperations.maximum([50,30,10,100,120])))
print("Minimum        of {}  is {}".format([50,30,10,100,120], ArithmeticOperations.minimum([50,30,10,100,120])))

print("\nArithmeticOpsBase Abstract Methods    : ", ArithmeticOpsBase.__abstractmethods__)
print("ArithmeticOperations Abstract Methods : ", ArithmeticOperations.__abstractmethods__)
Addition       of 10 & 20 is 30
Subtraction    of 10 & 20 is -10
Division       of 10 & 20 is 0.5
Multiplication of 10 & 20 is 200
Maximum        of [50, 30, 10, 100, 120]  is 120
Minimum        of [50, 30, 10, 100, 120]  is 10

ArithmeticOpsBase Abstract Methods    :  frozenset({'multiply', 'add', 'subtract', 'divide'})
ArithmeticOperations Abstract Methods :  frozenset()

Example 9

As a part of our ninth example, we have demonstrated how we can declare abstract property setter methods. We have created an abstract base class named PointBase which has 2 getter and 2 setter methods for setting __x and __y attributes of the class. We have declared both setter methods as abstract delegating their implementation on the class which implements abstract class. We have then implemented Point class which extends PointBase and implements its abstract methods. We have included a check in both setter methods which prevents setting values below -100.

We have then created an instance of the Point class and set/printed attributes X and Y to check the implementation of getters and setters.

In [15]:
from abc import ABC, ABCMeta
from abc import abstractmethod

class PointBase(ABC):
    @property
    def X(self):
        return self.__x

    @property
    def Y(self):
        return self.__y

    @X.setter
    @abstractmethod
    def X(self, newX):
        pass

    @Y.setter
    @abstractmethod
    def Y(self, newY):
        pass

class Point(PointBase):
    def __init__(self):
        self.__x, self.__y = 0, 0

    @property
    def X(self):
        return self.__x

    @property
    def Y(self):
        return self.__y

    @X.setter
    def X(self, newX):
        if newX < -100:
            raise ValueError("Negative Values Below -100 Not Accepted")
        self.__x = newX + 100

    @Y.setter
    def Y(self, newY):
        if newY < -100:
            raise ValueError("Negative Values Below -100 Not Accepted")
        self.__y = newY + 100

point = Point()

point.X, point.Y = 100, 150

print("Point ({}, {})".format(point.X, point.Y))

print("\nPointBase Abstract Methods    : ", PointBase.__abstractmethods__)
print("Point     Abstract Methods    : ", Point.__abstractmethods__)
Point (200, 250)

PointBase Abstract Methods    :  frozenset({'X', 'Y'})
Point     Abstract Methods    :  frozenset()

Below we are trying to access __x attribute of the class which fails as its private attribute and can be only accessed through getter and setters.

In [16]:
point.__x
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-16-0160183d2603> in <module>
----> 1 point.__x

AttributeError: 'Point' object has no attribute '__x'
In [17]:
print("Point Contents : ", point.__dict__)
Point Contents :  {'_Point__x': 200, '_Point__y': 250}

Below we are trying to set values below -100 as X and Y to verify whether our check works or not.

In [18]:
point.X, point.Y = -120, 100
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-18-48ae37689804> in <module>
----> 1 point.X, point.Y = -120, 100

<ipython-input-15-e6e8507aa6c3> in X(self, newX)
     36     def X(self, newX):
     37         if newX < -100:
---> 38             raise ValueError("Negative Values Below -100 Not Accepted")
     39         self.__x = newX + 100
     40

ValueError: Negative Values Below -100 Not Accepted

This ends our small tutorial explaining the usage of abc module to create an abstract base class and abstract methods. Python has introduced a module named collections.abc in version 3.3 which has a list of abstract base classes created for different kinds of data structures. All classes have a bunch of abstract methods that need implementation and enforce behavior of a particular type based on their implementation.

Please feel free to let us know your views in the comments section about the tutorial.



Sunny Solanki  Sunny Solanki