Abstract Classes and Meta Classes in Python

Abstract classes (or Interfaces) are an essential part of an Object-Oriented design. While Python is not a purely OOP language, it offers very robust solutions in terms of abstract and meta classes.

Abstract classes

In short, abstract classes are classes that cannot be instantiated. This is the case because abstract classes do not specify the implementation of features. Rather, you only specify the method signatures and (sometimes) property types, and every child class must provide its own implementation. Note that it is still possible to provide some implementation. For example, an abstract class can implement a default constructor.

Abstraction is very useful when designing complex systems to limit repetition and enforce consistency. For example, you may have the Renderer abstract class, with implementations 3DRenderer, 2DRenderer, HapticRenderer. Then, the code that is responsible for generating render data does not need to know about specific renderers: it will just refer to all of them as Renderer.

ABC module

In Python, abstraction is realized through the abc module in the built-in library. This is the simplest example of how to use it:

from abc import ABC

class AbstractRenderer(ABC):
    pass

The abc module exposes the ABC class, which stands for Abstract Base Class. Any class that inherits the ABC class directly, is, therefore, abstract. But there would not be much use for it without abstract methods:

from abc import ABC, abstractmethod

class AbstractRenderer(ABC):
    @abstractmethod
    def render(self, data):
        raise NotImplementedError()

In this example, we define the render abstract method, hence the @abstractmethod decorator. In its body, we raise the NotImplementedError as a safety precaution: in theory, it should not be possible to even call this method. If you try to instantiate AbstractRenderer, you will get an error:

>>> renderer = AbstractRenderer()
>>> Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: Can't instantiate abstract class AbstractRenderer with abstract methods render

Unless there is a concrete implementation of every abstractmethod in an abstract class, you will not be able to instantiate it. Here is how to implement it:

class 3DRenderer(AbstractRenderer):
    def render(self, data):
        some_library.draw_3d_stuff(data)

renderer = 3DRenderer()
renderer.render(some_data)

In addition to methods, it is possible to specify abstract properties (note that property is not part of abc):

class AbstractVehicle(ABC):
    @property
    @abstractmethod
    def engine(self):
        raise NotImplementedError()

    @engine.setter
    @abstractmethod
    def engine(self, _engine):
        raise NotImplementedError()

In this example, we define the engine abstract property. The first function (decorated with @property) is a getter, and the one decorated with @engine.setter is the setter. Later, you will have to implement them both:

class Car(AbstractVehicle):
    
    _engine = ''
    
    @property
    def engine(self):
        return self._engine
    
    @engine.setter
    def engine(self, new_engine):
        self._engine = new_engine.upper()

car = Car()
car.engine = 'v8 3.2 liters'
print(car.engine)
>> V8 3.2 LITERS

Take a closer look at the getter and the setter that is implemented here. Note that the setter changes the engine string to upper case. This is an example of using property to override default Python behaviour and its usage with abc.

Metaclasses

As you have just seen, Python’s implementation of abstract classes differs from what you see in other popular languages. This is because abstraction is not part of the language grammar, but instead is implemented as a library. The next logical question is, can you do something similar yourself, purely in Python? And the answer is yes.

Before moving to metaclasses, there is something you need to understand about classes first. You may think of classes as blueprints for object creation and you will be right. But, in Python, classes themselves are objects. For example, when running this code:

class YouExpectedMeToBeAClass:
    pass

Python will instantiate a class YouExpectedMeToBeAClass, and store this “object” in memory. Later, when you refer to this class when creating objects, python will use that “object”. But how does Python instantiate a class? Using a metaclass, of course.

Metaclasses are classes for classes. Metaclasses provide blueprints for classes creation. Every class has a metaclass by default (it is called type). To create a custom metaclass, you will have to inherit type:

class CustomMeta(type):
    pass

class SomeClass(metaclass=CustomMeta):
    pass

By itself, CustomMeta does nothing. Let’s add some more features to show you the power of metaclasses. Let’s make CustomMeta check if every child class has a render attribute (like with AbstractRenderer):

class CustomMeta(type):
    def __new__(cls, clsname, bases, attrs):
        if 'render' not in attrs.values():
            raise Exception()
        return type(clsname, bases, attrs)    

class SomeClass(metaclass=CustomMeta):
    pass

If you try to run this code (without even instantiating anything!) it will throw an error. Let me explain what __new__ is first. This is the constructor for classes, like __init__ for objects. It is called at the moment SomeClass is defined and whatever is returned from this function becomes the class. The arguments are following: the metaclass (cls), new classes’ name clsname, parent classes (bases) and attributes (attrs). In function body, we enumerate the attributes and check if render is one of them. If not, raise an exception. To make this code run, add this to SomeClass:

def render(self):
    pass

This is a very bare-bones example of what the abc module does under the hood. If you are interested to learn more, these are it’s source codes: python module, c implementation.

Closing notes

Thank you for reading, I hope you liked my article. Let me know if you have any use cases for metaclasses in the comments!

Resources

Get new content delivered to your mailbox:

leave a comment