Using a Metaclass to Keep Track of Subclasses

A few days ago I read Eli Bendersky’s blog post titled “Fundamental concepts of plugin infrastructures” which covers 4 fundamental aspects of any plugin infrastructure. It’s a great article, and it gives an example in Python which implements the concepts he talks about. One aspect of his example I’d like to focus on in this post is how he uses a metaclass to allow plugins (which are subclasses of a class using the metaclass) to register themselves in the system.

The following code is what I’ll be talking about:

class Registry(type):
    entries = []
    def __init__(cls, name, bases, attrs):
        if name != "Entry":
            Registry.entries.append(cls)

class Entry(metaclass=Registry):
    pass

Using this code we can keep track of subclasses of Entry:

>>> Registry.entries
[]
>>> class Example(Entry):
...     pass
...
>>> Registry.entries
[__main__.Example]

If we’re using this for a plugin system as in Eli’s case, then subclassing Entry is a way of registering the plugin with the system.

What’s going on here?

A metaclass is a class which instantiates classes. Since everything (including classes) in Python is an object, everything must have a class which instantiates it. As usual, we can use type() to discover this class.

>>> class X:
...     pass
...
>>> type(X)
<class 'type'>

This means that regular classes are actually instances of type, and this makes sense since we can use type() to instantiate a “type object” (i.e. a class). Doing so gives us an equivalent class to what we previously created:

>>> X = type('X', (object,), {})
>>> type(X)
<class 'type'>

So if we want to automatically keep track of classes which subclass Entry, we want to modify the class creation process to append the class to a list. Since type is the class from which regular classes are instantiated, we’ll do what we usually do when we want access to the object creation process: we’ll subclass type!

A Registry is a subclass of type, we can use it in the same way as we just use type():

>>> ExampleEntry = Registry("ExampleEntry", (object,), {})
>>> type(ExampleEntry)
__main__.Registry
>>> Registry.entries
[__main__.ExampleEntry]

As expected, the __init__ method of Registry was called, and we were able to save the class object that was passed in. It’s important to note that the first parameter of __init__ is the class after it was created, just an __init__ on regular classes gets the object after it was created; this is sufficient for our use case, so we won’t delve into the other magic methods.

The equivalent to the above syntax is the following:

class ExampleEntry(object, metaclass=Registry):
    pass

Since we want plugin authors to always subclass Entry, we gave Entry the metaclass parameter (which all subclasses will inherit), thus ensuring that all subclasses of Entry get automatically added to Registry.entries. Of course, we don’t want Entry itself in that list, so we only add classes which don’t have the name “Entry”.

More info

Hopefully this article provided a good use case for metaclasses and explained how it worked. For more information on metaclasses, I suggest Ionel Mărieș’s blog post on the topic as a good starting point since he also provides links to more articles in the first footnote.

Published on August 02, 2016.