HomeSoftwareAgencySuccess StoriesAmazon Expertise
ENFR

Real world metaclass usages inside django

Recently, here at Seelk, we faced an issue that motivated us to look at metaclasses and we discovered that it is a complicated subject confusing lots of people. This is even more true when you mix this with django’s own usage of metaclasses.
After eventually figuring things out (thanks to readings, trials and errors), we’re writing this article to clear up common misconceptions people might have about them. And what better way to show than a few real world examples ?

Before diving head first into the code and the actual examples, let’s first review together what actually is a meta class.

So, what is a metaclass ?

In order to quickly skim through the tedious details and jump directly to the parts we’re interested in, I’ll assume that you already did your homework and have some basic understanding of what is a metaclass in Python. If you don’t know anything about metaclasses, don’t worry, you can scroll down to the reference section, read the few listed articles and come back here as soon as you’re done.

The same way you’re using a class to define properties, methods and more on an object, a metaclass will be used to define the exact same things on a class.

A class is used to instanciate instances.
A metaclass is used to instanciate classes.
This is basically the gist of it.

Let's look at some examples to better understand how metaclasses are working.
Note that the following examples have no real world utility and that the same results could be achieved using simple inheritance. They're used to play with metaclasses not to justify their usages.

Let's start simple by changing an attribute and a class name

class RedAnimal:
    color = "red"

RedAnimal.color
# 'red'
RedAnimal.__name__
# 'RedAnimal'
type(RedAnimal)
# type

class MakeBlue(type):
    def __new__(cls, name, bases, attrs):
        attrs['color'] = "blue"
        return super().__new__(cls, 'BlueAnimal', bases, attrs)

class RedAnimal(metaclass=MakeBlue):
    color = "red"

RedAnimal.color
# 'blue'
RedAnimal.__name__
# 'BlueAnimal'
type(RedAnimal)
# __main__.MakeBlue

Nothing too shiny for now.
Let's spice things up a bit.

class Animal:
    def make_sound(self):
        return self.sound

class Dog(Animal):
    sound = "woof"

class Cat(Animal):
    sound = "meow"

Dog().make_sound()
# 'woof'
Cat().make_sound()
# 'meow'

class MakeLouder(type):
    def __new__(cls, name, bases, attrs):

        def make_into_loud_sound(func):
            def make_loud_sound(self):
                return f'{func(self)} !!!!!!!'
            return make_loud_sound 

        attrs['make_sound'] = make_into_loud_sound(attrs['make_sound'])
        return super().__new__(cls, f'Loud{name}', bases, attrs)

class Dog(Animal, metaclass=MakeLouder):
    sound = "woof"
# KeyError: 'make_sound'
# ----> attrs['make_sound'] = make_into_loud_sound(attrs['make_sound'])

Uh oh, what happened here ?
attrs refers to the attributes that are defined on Dog and if we look closely, Dog does not define anything besides sound.
make_soundis actually defined on Animal so we need to get the make_sound method from it and we can do it two ways.

For the first one we can get the method directly from the base class

class MakeLouder(type):
    def __new__(cls, name, bases, attrs):

        def make_into_loud_sound(func):
            def make_loud_sound(self):
                return f'{func(self)} !!!!!!!'
            return make_loud_sound 

        attrs['make_sound'] = make_into_loud_sound(bases[0].make_sound)
        return super().__new__(cls, name, bases, attrs)

class Dog(Animal, metaclass=MakeLouder):
    sound = "woof"

class Cat(Animal, metaclass=MakeLouder):
    sound = "meow"

Dog().make_sound()
# 'woof !!!!!!!'
Cat().make_sound()
# 'meow !!!!!!!'

For the second one we can simply move the metaclass to the Animal class

class MakeLouder(type):
    def __new__(cls, name, bases, attrs):

        def make_into_loud_sound(func):
            def make_loud_sound(self):
                return f'{func(self)} !!!!!!!'
            return make_loud_sound 
        print(name)
        attrs['make_sound'] = make_into_loud_sound(attrs['make_sound'])
        return super().__new__(cls, name, bases, attrs)

class Animal(metaclass=MakeLouder):
    def make_sound(self):
        return self.sound

class Dog(Animal):
    sound = "woof"
# KeyError: 'make_sound'
# ----> attrs['make_sound'] = make_into_loud_sound(attrs['make_sound'])

Whoops ! We got the same crash as before. But Why ?
If you were to put print(name) inside MakeLouder.__new__, you'd see two lines : Animal and Dog. The metaclass is actually called once for each class it is featured on. That means it is called when you're defining Animal but it is also called when you're defining Dog because it inherits from Animal which does feature the metaclass.

We can fix this two ways, right or wrong. We could add an if that checks that the name is 'Animal' before overriding make_sound but if Dog would be to redefine it then our change would not be taken into account so obviously, wrong way. Or we could simply add an if that checks that make_sound is inside attrs before overriding it which is the right way to go.

We end up with this

A few gotchas :

Beware of attributes defined on the bases instead of the class the metaclass is applied on

If a base class already uses a metaclass and your class must inherit this metaclass


What’s the difference with django’s class Meta ?


How to enhance a django model with a metaclass ?

Examples

Currency conversion


References :

Aaccessible StackOverflow answer of what is a metaclass in Python
Very deep article detailing the inner workings of metaclasses
Article that approaches metaclasses with examples in other libraries


Copyright 2020 © All rights reserved

Seelk New York

5th Avenue

001 938872 0238

Seelk Paris

7 - 11 Boulevard Haussman

0033 1873 378273