What are metaclasses

Objects are first-class citizens in python: every class is an object.

Every time we declare a class, under the hood python is actually instantiating a class that defines classes. Enter the meta-class.

Python’s metaclass is type, which, being itself a class, can be inherited and extended.

The signature of metaclass constructors is:

type(class_name, base_classes, attributes)

and with this call you can create a class definition anywhere.

Which means this:

class MyClass:
    atttribute1 = 'foobar'

could also be expressed like this:

MyClass = type('MyClass', (object, ), {'attribute1': 'foobar'})

Custom metaclasses

To define a metaclass we must inherit from a metaclass, so let’s start with type.

The __new__ method will be invoked during class declaration.

class MyMeta(type):
    def __new__(cls, name, bases, attributes):
        attributes['__metaclasses_are_awesome__'] = True

        cls = type.__new__(cls, name, bases, attributes)
        return cls
class MyClass(object, metaclass=MyMeta):
    pass

assert hasattr(MyClass, '__metaclasses_are_awesome__')
assert MyClass.__metaclasses_are_awesome__ == True

Pseudo-static checking

Let’s create a DSL to declare restful endpoints with classes.

We want to enforce the declaration of a route class-attribute being a string.

class ResourceMeta(type):
    def __new__(cls, name, bases, attributes):
        cls = type.__new__(cls, name, bases, attributes)

        if name in ('ResourceMeta', 'Resource'):
            # prevent processing the metaclass and resource baseclass.
            return cls

        route = attributes.get('route')
        if not route:
            raise TypeError(f'{cls} must declare a "route" attribute')

        if not isinstance(route, str):
            raise TypeError(
                f'{cls}.route must be a string, '
                f'got {type(route)} instead')
            )

        return cls


class Resource(object, metaclass=ResourceMeta):
    pass

class Settings(Resource):
    route = '/api/settings'

    def get(self):
        return {
            'foo': 'bar'
        }

Now, trying to declare a resource without a valid route:

class User(Resource):
    route = {'not a string'}

    def get(self):
        return {
            'email': 'john@doe.com'
        }

Would cause a TypeError during import time.