Metaprogramming refers to a programming technique that enables you to write code that can manipulate code. We’ve already discussed one form of metaprogramming supported by Python: decorators; this article will cover Python metaclasses. Before understanding metaclasses, you need to master classes in Python. I recommend reading these articles before continuing –
Borrowed from Smalltalk, Python has an odd idea of classes. Generally, classes are pieces of code that can be used to create objects. This is true for Python too, however, in Python, classes are objects too. Yes, objects. Everything in Python is an object, even classes.
a = 42 print(a.__class__) print(a.__class__.__class__) <type 'int'> <type 'type'> def func(): pass print(func.__class__) print(func.__class__.__class__) <type 'function'> <type 'type'> class XYZ(object): pass x = XYZ() print(x.__class__) print(x.__class__.__class__) <class '__main__.XYZ'> <type 'type'>
Python Metaclasses
Every object and class in Python is either an instance of a class or an instance of a metaclass. Every class inherits from the built-in basic base class object
, and every class is an instance of the metaclass type
. Except for type
, type
is its metaclass and base class (don’t ask “how?”, it’s done using an implementation level hack). Just like how a class defines the behaviour of its object, a metaclass defines the behaviour of classes. The main purpose of metaclasses is to change the behaviour of classes as soon as they are created.
Although you’ve probably never explicitly used metaclasses, they’re littered everywhere if you were to look under the hood. For instance, if you’ve ever created an abstract class in Python using the ABC
module, you indirectly inherited the ABCmeta
class. Or, if you’re a backend developer who uses Django, you’ve indirectly used the ModelBase
metaclass through model.Model
.
Much like how you can dynamically create objects of a class using the syntax: class_name()
, you create a class using the syntax: type()
. Let’s illustrate this with an example:
class Dummy(): x = 12 def meme(): print("When you try to define constants \nPython: We don’t do that here.") Dummy.meme() When you try to define constants Python: We don’t do that here.
The above syntax is equivalent to:
def nameless_func(): print("C++ – Can’t compare 'float' and 'int'. \nPython – Variable is variable.") WierdDummy = type('WierdDummy',() ,{'x':12, 'meme': nameless_func}) WierdDummy.meme() C++ – Can’t compare 'float' and 'int'. Python – Variable is variable.
Here, ‘WierdDummy
‘ is the new class’s name, ()
is a tuple containing the base class(es) that can be empty. {'x':12, 'meme': nameless_func}
is a dictionary that stores all class attribute names and values. At first glance, this syntax seems obscure and useless, and it mostly is, but it can be extremely powerful for niche metaprogramming use cases. Imagine this scenario: You have four unrelated mixin classes with different functionalities, and you need to create all possible combinations of two. Now you could write all 6 new classes manually or dynamically create them with a few lines of code.
class A: def show_a(self): print("Class A") class B: def show_b(self): print("Class B") class C: def show_c(self): print("Class C") class D: def show_d(self): print("Class D") from itertools import combinations for base_classes in combinations([A, B, C, D], 2): new_class_name = "".join([c.__name__ for c in base_classes]) globals()[new_class_name] = type(new_class_name , base_classes,{}) obj = AB() obj.show_a() obj.show_b() Class A Class B
Creating Metaclasses in Python
To create your own custom metaclasses in Python, you need to inherit type
, and to inherit from a custom metaclass; you need to explicitly specify it using metaclass=
. Let’s create a metaclass for enforcing the PEP8 naming convention for functions and variables.
from warnings import warn class EnforcePEP(type): def __new__(cls, clsname, bases, clsdct): new_dict = {} for attr, val in clsdct.items(): if attr.lower() != attr: warn(f"Function/Variable naming convention not followed! '{attr}' will now be '{attr.lower()}'") new_dict[attr.lower()] = val return type(clsname, bases, new_dict) class Example(metaclass = EnforcePEP): X = 12 nAme = "Dummy Class" def MAgic(self): print("Expelliarmus!") Warning (from warnings module): File "<pyshell#3>", line 6 UserWarning: Function/Variable naming convention not followed! 'X' will now be 'x' Warning (from warnings module): File "<pyshell#3>", line 6 UserWarning: Function/Variable naming convention not followed! 'nAme' will now be 'name' Warning (from warnings module): File "<pyshell#3>", line 6 UserWarning: Function/Variable naming convention not followed! 'MAgic' will now be 'magic'
obj = Example() obj.magic() Expelliarmus! print(obj.X) AttributeError: 'Example' object has no attribute 'X'
Last Epoch
This article discussed Python metaclasses, an abstruse OOP concept that lurks behind basically all Python code. The chances of you needing to use or create metaclasses are extremely low unless you’re creating a library of your own or complex APIs. And even then, most of your class augmentation needs can be satisfied by using decorators or simply monkey patching. That being said, not all object-oriented programming languages support metaclasses. It’s good to know that if the need arises Python provides the capability to define custom metaclasses. I highly recommend Mark Smith’s talk at PyCon AU 2019 if you want to learn more about Python metaclasses.