Cross-Python metaclasses

1 minute read

Using a class decorator for applying a metaclass in both Python 2 and 3

When you want to create a class including a metaclass, making it compatible with both Python 2 and 3 can be a little tricky.

The excellent six library provides you with a six.with_metaclass() factory function that’ll generate a base class for you from a given metaclass:

from six import with_metaclass

class Meta(type):
    pass

class Base(object):
    pass

class MyClass(with_metaclass(Meta, Base)):
    pass

The basic trick is that you can call any metaclass to produce a class for you, given a name, a sequence of baseclasses and the class body. six produces a new, intermediary base class for you:

>>> type(MyClass)
<class '__main__.Meta'>
>>> MyClass.__mro__
(<class '__main__.MyClass'>, <class 'six.NewBase'>, <class '__main__.Base'>, <type 'object'>)

This can complicate your code as for some usecases you now have to account for the extra six.NewBase baseclass present.

Rather than creating a base class, I’ve come up with a class decorator that replaces any class with one produced from the metaclass, instead:

def with_metaclass(mcls):
    def decorator(cls):
        body = vars(cls).copy()
        # clean out class body
        body.pop('__dict__', None)
        body.pop('__weakref__', None)
        return mcls(cls.__name__, cls.__bases__, body)
    return decorator

which you’d use as:

class Meta(type):
    pass

class Base(object):
    pass

@with_metaclass(Meta)
class MyClass(Base):
    pass

which results in a cleaner MRO:

>>> type(MyClass)
<class '__main__.Meta'>
>>> MyClass.__mro__
(<class '__main__.MyClass'>, <class '__main__.Base'>, <type 'object'>)

Update

As it turns out, Jason Coombs took Guido’s time machine and added the same functionality to the six library last summer. Not only that, he included support for classes with __slots__ in his version. Thanks to Mikhail Korobov for pointing this out.

The six decorator is called @six.add_metaclass():

@six.add_metaclass(Meta)
class MyClass(Base):
    pass

Leave a comment