How to use classes and metaclasses in Python

As an Amazon Associate I earn from qualifying purchases.

5 Min Read

Today lets take about how to use classes and metaclasses in Python.

Everything in Python is an Object

To understand metaclasses – we first need to understand how a class is created. In Python, classes are objects. Not only do classes create objects in Python but classes themselves are objects. And actually, everything in Python is an object (which I will prove).

Let’s say we have the following code in Python:

class MyClass:
	pass

def myFunction():
	pass

my_dict = {}

Above, I have a very basic class, function and a dictionary. Let’s get the type of each using type()

print(type(MyClass()))
print(type(my_function))
print(type(my_dict))

When you run this code, you will get the following output:

<class '__main__.MyClass'>
<class 'function'>
<class 'dict'>

Here you can see that everything in Python is an object, even functions!

So an instance of MyClass has a type of MyClass. What happens when we just pass the class?

class MyClass:
	pass

def my_function():
	pass

my_dict = {}

print(type(MyClass()))
print(type(my_function))
print(type(my_dict))
print(type(MyClass))

Output:

<class '__main__.MyClass'>
<class 'function'>
<class 'dict'>
<class 'type'>

You can see above that a Class itself itself is a type. Confused? Don’t worry I will explain.

A class defines the rules for an object. A class is created from a type. When we create a class in Python the type constructor is used to create that class. In other words, type is the default metaclass for all classes.

Creating a class from the type constructor

Let’s go back to our original example. In this example, I created a new class named MyClass:

class MyClass:
	pass

I mentioned that when we define a class, the type constructor is called. This means that we can also create a class using the type constructor:

MyClass = type('MyClass', (), {})

Both of these snippets of code are identical in functionality.

The type constructor is defined as follows:

type(<name of class>, <base class (if any)>, <attributes>)

Adding variables to a class using the type constructor

Okay so how do we define a variable for a class using the type constructor? A variable would go inside the attributes dictionary:

MyClass = type('MyClass', (), {"x" : 1})

Now, lets create a new instance of my class and print x:

MyClass = type('MyClass', (), {"x" : 1})
my_class = MyClass()
print(my_class.x)

Output:

$ python metaclass.py
1

You can see that when we print out x it is 1.

Adding functions to a class using the type constructor

To add a function to a class using the type constructor, we will first need to define one:

def my_function():
	print("My function!")

Now, add this to the attributes dictionary after the x entry:

def my_function(self):
	print("My function!")

MyClass = type('MyClass', (), {"x" : 1, "my_function": my_function})

Again, create a new instance of MyClass and call my_function() this time:

def my_function(self):
	print("My function!")

MyClass = type('MyClass', (), {"x" : 1, "my_function": my_function})
my_class = MyClass()
my_class.my_function()

Output:

$ python metaclass.py
My function!

As you can see, calling the function works as expected and printed “My function!”

Summary of Classes

In summary, a class can be defined either by using the keyword class or by using the type constructor. Metaclasses such as type take your class and convert it into an object that represents the current class. The default metaclass for a class is type.

Creating your own metaclass

As mentioned before a class defines the attributes, methods etc. of an object. Conversely, a metaclass defines the rules for a class. A class uses the type metaclass by default.

Now that you have some knowledge of how classes are constructed, let’s create a new metaclass for our class MyClass. Start by creating a regular class which takes type:

class MyMetaClass(type):

__new__ and __init__ explaned

When a class is created, two functions will be called: __new__ and __init__ in this order.

__new__ is responsible for returning a new instance of your class.

__init__ is responsible for initializing the instance after it has been created.

Modifying the custom metaclass

We want to change how a new instance of our class is returned. To do this we will need to override the __new__ method. Start by defining the __new__ method which takes the following arguments: self, class_name, base, attr:

class MyMetaClass(type):
	def __new__(self, class_name, bases, attr):

You will notice that the __new__ contains the same arguments as the type constructor. This is because we will be returning type to create the class object. To modify the construction of our class, we will place a print statement above where type is being returned. This means that for every class created using this metaclass, it will call the print statement:

class MyMetaClass(type):
	def __new__(self, class_name, base, attr):
		print("Creating class: ", class_name)
		return type(class_name, base, attr)

Now it is time to rebuild MyClass using the MyMetaClass as the metaclass:

class MyClass(metaclass = MyMetaClass):

You will notice above that we changed the default metaclass to MyMetaClass. Now every time this object is initialized, it will use our custom metaclass! To complete this class lets add some variables and a function and initialize the class:

class MyClass(metaclass = MyMetaClass):
	x = 1
	
	def my_function(self):
		print("My function!")

my_class = MyClass()

Now lets see what happens when we initialize MyClass:

$ python metaclass.py
Creating class:  MyClass

Because we changed how a new instance of our class was returned by adding a print statement, it will be printed every time we initialize a class that is using our custom metaclass.

Now, I will show you a more useful use case. Let’s say that you wanted to add a new variable to any class that uses this metaclass, regardless if it was defined in the class or not, we can achieve this with metaclasses. Simply add to the attr dictionary a new key and value. (Note there are easier ways of achieving this effect – this is purely for demonstration purposes):

class MyMetaClass(type):
	def __new__(self, class_name, base, attr):
		print("Creating class: ", class_name)
		
		attr['custom_var'] = 5
		
		return type(class_name, base, attr)

class MyClass(metaclass = MyMetaClass):
	x = 1
	
	def my_function(self):
		print("My function!")

When you initialize MyClass again you will have access to the variable custom_var created in the metaclass and you can print out the value:

my_class = MyClass()
print(my_class.custom_var)

Output:

$ python metaclass.py
Creating class:  MyClass
5

As you can see, even though we did not define custom_var in MyClass we still have access to it because our metaclass modified it during the creation of the class object.

Classes and particularly metaclasses can be confusing but I hope this article helps demystify both concepts and how classes and metaclasses are related.

That’s all for classes and metaclasses in Python! As always, if you have any questions or comments please feel free to post them below. Additionally, if you run into any issues please let me know.

Make sure to check out these other Python tutorials 🙂

If you’re interested in learning Python I highly recommend this book. In the first half of the book, you”ll learn basic programming concepts, such as variables, lists, classes, and loops, and practice writing clean code with exercises for each topic. In the second half, you”ll put your new knowledge into practice with three substantial projects: a Space Invaders-inspired arcade game, a set of data visualizations with Python”s handy libraries, and a simple web app you can deploy online. Get it here.

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to my newsletter to keep up to date with my latest posts

Holler Box