62

I want to ask what the with_metaclass() call means in the definition of a class.

E.g.:

class Foo(with_metaclass(Cls1, Cls2)):
  • Is it a special case where a class inherits from a metaclass?
  • Is the new class a metaclass, too?
import this
  • 517
  • 2
  • 7
  • 21
Zakos
  • 2,448
  • 4
  • 16
  • 20

2 Answers2

89

with_metaclass() is a utility class factory function provided by the six library to make it easier to develop code for both Python 2 and 3.

It uses a little sleight of hand (see below) with a temporary metaclass, to attach a metaclass to a regular class in a way that's cross-compatible with both Python 2 and Python 3.

Quoting from the documentation:

Create a new class with base class base and metaclass metaclass. This is designed to be used in class declarations like this:

from six import with_metaclass
   
class Meta(type):
    pass

class Base(object):
    pass

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

This is needed because the syntax to attach a metaclass changed between Python 2 and 3:

Python 2:

class MyClass(object):
    __metaclass__ = Meta

Python 3:

class MyClass(metaclass=Meta):
    pass

The with_metaclass() function makes use of the fact that metaclasses are a) inherited by subclasses, and b) a metaclass can be used to generate new classes and c) when you subclass from a base class with a metaclass, creating the actual subclass object is delegated to the metaclass. It effectively creates a new, temporary base class with a temporary metaclass metaclass that, when used to create the subclass swaps out the temporary base class and metaclass combo with the metaclass of your choice:

def with_metaclass(meta, *bases):
    """Create a base class with a metaclass."""
    # This requires a bit of explanation: the basic idea is to make a dummy
    # metaclass for one level of class instantiation that replaces itself with
    # the actual metaclass.
    class metaclass(type):

        def __new__(cls, name, this_bases, d):
            return meta(name, bases, d)

        @classmethod
        def __prepare__(cls, name, this_bases):
            return meta.__prepare__(name, bases)
    return type.__new__(metaclass, 'temporary_class', (), {})

Breaking the above down:

  • type.__new__(metaclass, 'temporary_class', (), {}) uses the metaclass metaclass to create a new class object named temporary_class that is entirely empty otherwise. type.__new__(metaclass, ...) is used instead of metaclass(...) to avoid using the special metaclass.__new__() implementation that is needed for the slight of hand in a next step to work.
  • In Python 3 only, when temporary_class is used as a base class, Python first calls metaclass.__prepare__() (passing in the derived class name, (temporary_class,) as the this_bases argument. The intended metaclass meta is then used to call meta.__prepare__(), ignoring this_bases and passing in the bases argument.
  • next, after using the return value of metaclass.__prepare__() as the base namespace for the class attributes (or just using a plain dictionary when on Python 2), Python calls metaclass.__new__() to create the actual class. This is again passed (temporary_class,) as the this_bases tuple, but the code above ignores this and uses bases instead, calling on meta(name, bases, d) to create the new derived class.

As a result, using with_metaclass() gives you a new class object with no additional base classes:

>>> class FooMeta(type): pass
...
>>> with_metaclass(FooMeta)  # returns a temporary_class object
<class '__main__.temporary_class'>
>>> type(with_metaclass(FooMeta))  # which has a custom metaclass
<class '__main__.metaclass'>
>>> class Foo(with_metaclass(FooMeta)): pass
...
>>> Foo.__mro__  # no extra base classes
(<class '__main__.Foo'>, <type 'object'>)
>>> type(Foo) # correct metaclass
<class '__main__.FooMeta'>
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 2
    Just to clarify, in six, the syntax (to match the behaviour of python 2 and 3 above): `class MyClass(with_metaclass(Meta, object)): pass` (where the object is optional). – Andy Hayden Apr 18 '15 at 04:59
  • If you don't want the `Base` class in your hierarchy, you can just do `MyClass = with_metaclass(Meta, MyClass)` after declaring `MyClass`, right? This has the same caveats as the `add_metaclass` solution below I think (double class creation). – DylanYoung Aug 19 '19 at 15:03
  • 1
    @DylanYoung: You don't need to bother. `with_metaclass()` was updated about a year after I wrote this answer to avoid having an extra class in the class inheritance hierarchy. A temporary class is still created, but only to intercept the class factory procedure. – Martijn Pieters Aug 19 '19 at 16:07
28

UPDATE: the six.with_metaclass() function has since been patched with a decorator variant, i.e. @six.add_metaclass(). This update fixes some mro issues related to the base objects. The new decorator would be applied as follows:

import six

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

Here are the patch notes and here is a similar, detailed example and explanation for using a decorator alternative.

pylang
  • 40,867
  • 14
  • 129
  • 121
  • Thanks! This does not have issues with `isinstance` unlike every other way to have Py2/3 compatible metaclasses. – Jérémie Aug 07 '19 at 03:21
  • The decorator only works in certain scenarios in my experience, since it modifies/copies the class after creation, whereas the whole point of a metaclass is to determine how the class is created. I forget what the exact errors were, but it was a pain; I would avoid it. – DylanYoung Aug 16 '19 at 14:02
  • @pylang Thanks for the tip! I wish I could remember the trouble I was having. If I encounter it again, I'll file a bug report. – DylanYoung Aug 18 '19 at 23:54
  • @pylang In general though, I don't think these problems can really be solved. The fact is that a class decorator runs *after* a class is created. That means any class with side-effects on creation will trigger those side-effects *twice*. If you think it's solvable using a class decorator approach, I'd love to hear how. In case it helps, I think I came across the error creating a `ProxyModelBase` metaclass in Django. – DylanYoung Aug 19 '19 at 14:42
  • @pylang In fact, this is mentioned right in the patch notes: "This is pretty nice. It does have the slightly non-intuitive effect that the class is created without the metaclass first. This might cause problems if the parent has a metaclass that is a superclass of the metaclass used with the decorator. That's also quite rare, so it can probably be ignored." – DylanYoung Aug 19 '19 at 14:48
  • Considering this error (part of the python language design) "metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases", I'm not sure why the reviewer thinks this should be "quite rare" – DylanYoung Aug 19 '19 at 14:56