Abstract classes are classes that do not have a complete implementation. They define a set of abstract methods, properties, or both that must be implemented by concrete subclasses.

This is a slightly advanced topic and you should be already familiar with function decorators and have a  knowledge on how python classes work.

Consider the simple example shown below:

class Abstract:
   def some_method(self):
      raise NotImplementedError

a = Abstract()
a.some_method()

In the above case whenever the method is called from the base class a NotImplementedError is raised. We will look at this error and how it works in a moment.

An abstract method is a method that has a declaration, but no implementation. Typically any method that raises the NotImplementedError would be regarded as an abstract method.

Subclasses of an abstract class  are required to implement the inherited abstract methods.

class Abstract:
   def some_method(self):
      raise NotImplementedError

class Example(Abstract):
   def some_method(self):
       print("This method has been implemented in the subclass")

a = Example()
a.some_method()

A class that contains one or more abstract methods is considered an abstract class .

If loosely defined, the above class, "Abstract", may pass as an abstract class. However, in strict definition  it is merely just a normal class.

One key and defining feature of abstract base class is that it cannot be instantiated, which means that an object cannot be created from it. Instead, other classes extend the abstract base class and implement the abstract methods. Python, by default does not offer this type of functionality for abstract classes. However, the abc module in the standard library provides the infrastructure for defining abstract base classes (ABCs) and for managing relationships between classes and their subclasses. Let us look at this module and how we can use it to define "real" abstract base classes.

The abc module

The abc(abstract base class) module provides a way to ensure that a particular abstract method is overridden when inheriting from an abstract class. in case the subclass does not override the inherited abstract methods, the abc module will raise a TypeError when the subclass is instantiated.This is essential to ensure that the intended behavior is enforced when using the abstract class as a base class.

Creating Base Class involves two essential steps:

  1. The abstract class should subclass the ABC class in the abc module.
  2. Abstract methods or properties should be implemented using the various decorators defined in the abc module
#import the ABC class and the abstractmethod decorator from the abc module
from abc import ABC, abstractmethod

#Define the abstract class
class Abstract(ABC):

   #The following abstract method must be implemented by the subclass. Otherwise, a TypeError will be raised.
   @abstractmethod
   def some_method(self):
      pass

#Define the subclass method
class Example(Abstract):
   def other_method(self):
       print("just a method.")

#An TypeError is raised on trying to create an object from the class because it still contains abstract methods.
a = Example()

The abstract method should be implemented as follows:

#import the ABC class and the abstractmethod decorator from the abc module
from abc import ABC, abstractmethod

#Define the abstract class
class Abstract(ABC):
  
   @abstractmethod
   def some_method(self):
      pass

#Define the subclass
class Example(Abstract):

   def some_method(self):
       print("This method has been implemented by the subclass. No error will be raised.")

#It is now possible to create instances
a = Example()
a.some_method()

The @abstractmethod decorator is used when we want to create abstract Instance methods. These are methods that are associated with class instances rather than the class itself. You can check on instance, class and static methods to see how each of them works.

Defining  abstract class methods

Class methods unlike instance methods are associated with the whole class rather than instances.These methods can  be called from the class itself or from its instances They are defined using the @classmethod decorator.

The @abstractclassmethod is used to define abstract class methods.

#import the ABC class and the abstractclassmethod decorator from the abc module
from abc import ABC, abstractclassmethod

#Define the abstract class
class Abstract(ABC):
  
   @abstractclassmethod
   def some_method(self):
      pass

#Define the subclass
class Example(Abstract):
   x = 10

   @classmethod
   def some_method(cls):
      print("The value of the class variable 'x' is, %s"%cls.x) 

Example.some_method()
e = Example()
e.some_method()

Defining abstract static methods

Static methods are utility methods, they cannot access object or class data.Tthey are defined using the @staticmethod decorator and can be accessed from the class itself or from instances.

The @abstractstaticmethod decorator in abc module is used to define abstract static methods.

#import the ABC class and the abstractstaticmethod decorator from the abc module
from abc import ABC, abstractstaticmethod

#Define the abstract class
class Abstract(ABC):
  
   @abstractstaticmethod
   def some_method(self):
      pass

#Define the subclass
class Example(Abstract):
   x = 10

   @staticmethod
   def some_method():
      print("I am a utility method.") 

Example.some_method()
e = Example()
e.some_method()

Final example

from abc import ABC, abstractmethod, abstractclassmethod, abstractstaticmethod

from math import pi

class Shape(ABC):
   
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass
  
    @abstractstaticmethod
    def calculate_area():
         pass
    
    @abstractstaticmethod
    def calculate_perimeter():
        pass

class Circle(Shape):

    def __init__(self, r):
        self.radius = r

    def area(self):
        return self.calculate_area(self.radius)
    
    def perimeter(self):
        return self.calculate_perimeter(self.radius)
  
    @staticmethod
    def calculate_area(r):
       return pi * r * r
    
    @staticmethod
    def calculate_perimeter(r):
        return 2 * pi *r

class Rectangle(Shape):

      def __init__(self, w, l):
          self.width = w
          self.length = l

      def area(self):
          return self.calculate_area(self.width, self.length)
    
      def perimeter(self):
          return self.calculate_perimeter(self.width, self.length)
  
      @staticmethod
      def calculate_area(w, l):
          return w * l
    
      @staticmethod
      def calculate_perimeter(w, l):
          return 2 * (w + l)

c = Circle(7)
print(c.area())
print(c.perimeter())

r = Rectangle(100, 250)
print(r.area())
print(r.perimeter())