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'll now explain with simple examples how we can create abstract base classes using Python.
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.
from abc import ABC
class MappingBase(ABC):
pass
print("MappingBase Abstract Methods : ", MappingBase.__abstractmethods__)
mb = MappingBase()
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.
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()
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.
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()
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.
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)
mb
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.
mb[10] = 10
try:
mb["X"] = 2.45
except Exception as e:
print(e)
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.
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.
class ArithmeticOperations(ArithmeticOpsBase):
pass
print("\nArithmeticOpsBase Abstract Methods : ", ArithmeticOpsBase.__abstractmethods__)
print("ArithmeticOperations Abstract Methods : ", ArithmeticOperations.__abstractmethods__)
ao = ArithmeticOperations()
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.
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()
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.
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__)
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.
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__)
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).
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__)
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.
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__)
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.
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__)
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.
point.__x
print("Point Contents : ", point.__dict__)
Below we are trying to set values below -100 as X and Y to verify whether our check works or not.
point.X, point.Y = -120, 100
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.
If you are more comfortable learning through video tutorials then we would recommend that you subscribe to our YouTube channel.
When going through coding examples, it's quite common to have doubts and errors.
If you have doubts about some code examples or are stuck somewhere when trying our code, send us an email at coderzcolumn07@gmail.com. We'll help you or point you in the direction where you can find a solution to your problem.
You can even send us a mail if you are trying something new and need guidance regarding coding. We'll try to respond as soon as possible.
If you want to