Use Cases for Metaclasses in Python

Rajesh PetheRajesh Pethe
3 min read

Why do we even need metaclasses? Specially when we have class inheritance and multiple inheritance and constructors to granularly define what we need?

Answer is yes! We need metaclass when we need to influence how a class is defined. In other words - when we need to mutate a class into doing something special.

While most Python applications might never need to use metaclasses, here is a list of use cases I can think off:

  1. Registration

  2. Refactoring or modifying class attributes

  3. Wrapping class methods

Registration of classes

models = {}


class ModelMetaclass(type):
    def __new__(cls, name, bases, namespace):
        cls = type.__new__(cls, name, bases, namespace)
        # No need to register `base` class
        if name != "Model":
            models[name] = cls
        return cls


class Model(metaclass=ModelMetaclass):
    pass

Any class subclassing Model, will be registered in the models dictionary.



class Foo(Model):
    pass


class Bar(Model):
    pass


print(models.keys())
['Model', 'Foo', 'Bar']

This can be achieved quite easily using class decorators but, imagine using class decorators with every class. So inheritance makes using metaclass advantageous.


NOTE

A metaclass's __new__ function gets a set of parameters -

  • child class, name of the child class.

  • bases is a tuple containing all parent classes.

  • child class's namespace which is a dict of all attributes and methods defined in class.

A class's namespace becomes important when you want to alter or use class's attributes and methods.


Refactoring or modifying class attributes

Take a look at django ORM's ModelBase and its __new__ method. It is a metaclass which is base class for all django models.

Here is an example of doing something similar:

class ModelMetaclass(type):
    def __new__(cls, name, bases, namespace):
        fields = {}
        for key, value in namespace.items():
            if isinstance(value, Field):
                value.name = f"{name}.{key}"
                fields[key] = value
        namespace["_fields"] = fields
        return type.__new__(meta, name, bases, namespace)

class Model(metaclass=ModelMetaclass):
    pass

This adds a field's dot separated name and also adds a _fields dictionary to namespace to keep track of all fields. One can iterate over bases (base classes) and do the same.

Wrapping class methods

This is useful if you need to execute some code or add extra functionality to class methods. And example would be to track run time a long running method or adding debug information.

Here is an example:

from functools import wraps
import time


class ProfilerMeta(type):

    def get_wrapper(func, *args, **kwargs):

        @wraps(func)
        def runtime_wrapper(*args, **kwargs):
            start = time.perf_counter()
            rv = func(*args, **kwargs)
            stop = time.perf_counter()
            runtime = stop - start
            print(f"Finished executing {func.__name__} in {runtime} seconds")
            return rv

        return runtime_wrapper

    def __new__(cls, name, bases, namespace):
        for name, func in list(namespace.items()):
            if callable(func):
                namespace[name] = cls.get_wrapper(func)

        return type.__new__(cls, name, bases, namespace)


class Foo(metaclass=ProfilerMeta):
    def long_running_method(self):
        time.sleep(5)
        print("Finished long_running_method")

    def another_long_running_method(self):
        time.sleep(3)
        print("Finished Another long_running_method")


foo = Foo()
foo.long_running_method()
foo.another_long_running_method()

Output:

Finished long_running_method
Finished executing long_running_method in 5.004354310003691 seconds
Finished Another long_running_method
Finished executing another_long_running_method in 3.003058955000597 seconds

What is the metaclass ProfilerMeta doing here is, iterating over namespace dict of the child class. It then wraps all functions it can find (long_running_method and another_long_running_method in this case). It wraps it with a wrapper function runtime_wrapper which calculates the runtime while executing the method.

Conclusion

There is no doubt that using metaclasses is like adding some magic to Python classes, which might make things a little more complex. But there are cases where you just need it to achieve, singleton design pattern is another use case.

0
Subscribe to my newsletter

Read articles from Rajesh Pethe directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rajesh Pethe
Rajesh Pethe

Passionate software engineer with 17+ years of experience in design and development of full life cycle commercial applications. Functional experience include Financial, Telecom and E-Commerce applications. Primary technical stack includes but not limited to Python, Django, REST, SQL, Perl, Unix/Linux. Secondary technical skills include Java, Angular and React JS. DevOps skills include CiCD, AWS, Docker, Kubernetes and Terraform.