What are metaclasses in Python

Pratik Choudhari

Metaclass is an advanced Python concept and is not used in day-to-day programming tasks. They allow a user to precisely define a class state, change its attributes and control method behavior.

In this article, we will understand how metaclasses can be created and used in Python.

1. Type and OOP

Everything is an object in Python, including classes. Hence, if classes are an object, they must be created by another class also called as Metaclass.

class hierarchy

So, a metaclass is just another class that creates class objects. Usually, type is the built-in metaclass Python uses for every class. But we can also create our own metaclasses with custom behavior.

2. Understanding the type() function

To fully understand Metaclasses, one needs to thoroughly understand type().

In Python, type() is used to identify the data type of a variable. If we have the following code

var = 12 print(type(var))

Console output would be:

<class 'int'>

Indicating var is an instance of the class int.

In the case of a user-defined class, we get the following output.

class Foo: def bar(): pass foo_obj = Foo() print("Type of Foo's instance is:", type(foo_obj)) print("Type of Foo is:", type(Foo))
Type of Foo's instance is: <class '__main__.Foo'> Type of Foo is: <class 'type'>

Here, we can see foo_obj is correctly identified as an instance of class Foo, but the type of class Foo indicates that it is an instance of class type, establishing the point that classes are indeed created by other classes.

Creating classes dynamically using type()

This is one more concept we should know before diving into metaclasses.

This usage of type() is fairly unknown. The type() function can take three arguments and can be used to create classes dynamically:

type(name: str, bases: tuple, attributes: dict)

Example:

Dummy = type("Dummy", (), {"var1": "lorem ipsum sit domat"}) dummy = Dummy() print(dummy.var1)

Output:

lorem ipsum sit domat

3. Creating Metaclass

The main purpose of a metaclass is to change the class automatically when it's created.

To use class as a metaclass we pass metaclass=ClassName as a class parameter. The metaclass itself is implemented like a normal class with any behavior we want. Usually the __new__ function is implemented.

The name of the metaclass should ideally make the intent of the class clear:

Example:

class PrintAttributeMeta: def __new__(cls, class_name, bases, attributes): print("Attributes received in PrintAttributeMeta:", attributes) return type(class_name, bases, attributes) class Dummy(metaclass=PrintAttributeMeta): class_var1 = "lorem" class_var2 = 45

Output:

Attributes received in PrintAttributeMeta: {'__module__': '__main__', '__qualname__': 'Dummy', 'class_var1': 'lorem', 'class_var2': 45}

This meta class simply prints all attributes, and then creates and returns a new class by using the type() function.

Note that there’s output without creating an instance of the class because the __new__() method is called when the class is created.

4. Modifying a class state using Metaclass

Using Metaclasses, the user can perform multitude of validations and modification such as changing variable names, raising error on receiving an invalid data type, checking inheritance levels, etc.

For this example, we will write a Metaclass that raises a ValueError when a negative value is received in attributes.

class PositiveNumbersMeta: def __new__(cls, class_name, bases, attributes): for attr, value in attributes.items(): if isinstance(value, (int, float)) and value < 0: raise ValueError(f"Negative values are not allowed in {class_name}") return type(class_name, bases, attributes) class MyClass(metaclass=PositiveNumbersMeta): class_var1 = 23 class_var2 = -1

Output:

Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in __new__ ValueError: Negative values are not allowed in MyClass

We could also change attributes:

class PositiveNumbersMeta: def __new__(cls, class_name, bases, attributes): for attr, value in attributes.items(): if isinstance(value, (int, float)) and value < 0: attributes[attr] = -value return type(class_name, bases, attributes) class MyClass(metaclass=PositiveNumbersMeta): class_var1 = 23 class_var2 = -1 a = MyClass() print(a.class_var2) # 1

5. Use proper OOP principles

In the previous example we are calling type directly and aren't overriding or calling the parent's __new__. Let's do that instead:

class PositiveNumbersMeta(type): def __new__(cls, class_name, bases, attributes): for attr, value in attributes.items(): if isinstance(value, (int, float)) and value < 0: attributes[attr] = -value return super.__new__(class_name, bases, attributes)

Now our metaclass inherits from type, and by calling the super.__new__() we are using the OOP syntax and call the __new__() function of the base class.

6. When to use metaclasses?

There are several reasons to do so:

Resource: Here's a great StackOverflow answer that explains it well.

Note that Metaclasses are usually only used in more complex scenarios, and in 99% of cases you don't have to worry about it. One common use case is when creating an API. A typical example of this is the Django ORM.

FREE VS Code / PyCharm Extensions I Use

✅ Write cleaner code with Sourcery, instant refactoring suggestions: Link *

* This is an affiliate link. By clicking on it you will not have any additional costs, instead you will support me and my project. Thank you! 🙏

Check out my Courses