Skip to content

Instantly share code, notes, and snippets.

@AlexWaygood
Created April 14, 2026 10:44
Show Gist options
  • Select an option

  • Save AlexWaygood/40244f5fdc9e7791d76eafab91ce1b7e to your computer and use it in GitHub Desktop.

Select an option

Save AlexWaygood/40244f5fdc9e7791d76eafab91ce1b7e to your computer and use it in GitHub Desktop.
Claude walkthrough of `metaclass=` handling

How CPython handles metaclass= in class statements

This walks through the full lifecycle of a metaclass= keyword argument, from the bytecode the compiler emits for a class statement down to the metaclass resolution logic in builtins.__build_class__ and _PyType_CalculateMetaclass. All permalinks pin to commit d931725bc850cd096f6703bc285e885f1e015f05.

1. The compiler lowers class to a call to __build_class__

metaclass= has no dedicated syntax or opcode. The compiler treats it as a plain keyword argument to a synthetic call. codegen_class_body in Python/codegen.c documents the shape of the lowered call at the top of the function:

/* ultimately generate code for:
     <name> = __build_class__(<func>, <name>, *<bases>, **<keywords>)
   where:
     <func> is a zero arg function/closure created from the class body.
        It mutates its locals to build the class namespace.
     <name> is the class name
     <bases> is the positional arguments and *varargs argument
     <keywords> is the keyword arguments and **kwds argument
   This borrows from codegen_call.
*/

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/codegen.c#L1560-L1568

The compiler then pushes LOAD_BUILD_CLASS and the class body closure onto the stack:

/* 2. load the 'build_class' function */
// these instructions should be attributed to the class line,
// not a decorator line
loc = LOC(s);
ADDOP(c, loc, LOAD_BUILD_CLASS);
ADDOP(c, loc, PUSH_NULL);

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/codegen.c#L1646-L1652

So class Foo(Base, metaclass=Meta, extra=1): ... is compiled to the moral equivalent of Foo = __build_class__(<body_func>, 'Foo', Base, metaclass=Meta, extra=1). metaclass is not special to the bytecode; it rides inside the ordinary **kwds.

2. __build_class__ drives the whole class-creation protocol

The implementation lives at Python/bltinmodule.c in builtin___build_class__. Its header summarises the contract:

__build_class__(func, name, /, *bases, [metaclass], **kwds) -> class

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L249-L252

2a. PEP 560 __mro_entries__ runs before metaclass resolution

The original base tuple is preserved, then rewritten via update_bases so that any non-type bases (e.g. typing.Generic[T]) get a chance to contribute real types through __mro_entries__. This matters for metaclass resolution, because the metaclass of a base is computed from the rewritten bases:

orig_bases = PyTuple_FromArray(args + 2, nargs - 2);
if (orig_bases == NULL)
    return NULL;

bases = update_bases(orig_bases, args + 2, nargs - 2);

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L127-L135

update_bases walks each positional base; for anything that isn't a PyType_Check and that exposes __mro_entries__, it calls base.__mro_entries__(bases) and splices the returned tuple in place. If any replacement happened, __orig_bases__ is later attached to the namespace (see 2e).

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L28-L98

2b. Pop metaclass from the keyword-argument dict

__build_class__ converts the kwnames stack slice into a dict (mkw) and then removes the metaclass key from it. This is why metaclass= never reaches __prepare__ or the metaclass' __init__ as a keyword argument (it's consumed here), while every other class keyword does get forwarded unchanged.

if (kwnames == NULL) {
    meta = NULL;
    mkw = NULL;
}
else {
    mkw = _PyStack_AsDict(args + nargs, kwnames);
    if (mkw == NULL) {
        goto error;
    }

    if (PyDict_Pop(mkw, &_Py_ID(metaclass), &meta) < 0) {
        goto error;
    }
    if (meta != NULL) {
        /* metaclass is explicitly given, check if it's indeed a class */
        isclass = PyType_Check(meta);
    }
}

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L137-L154

Two things worth noting:

  • The isclass flag records whether the user-supplied metaclass is itself a class — PyType_Check(meta) is PyType_FastSubclass(Py_TYPE(meta), Py_TPFLAGS_TYPE_SUBCLASS), i.e. it asks whether meta's metatype is a subclass of type, which is the C-level spelling of isinstance(meta, type). So isclass is true exactly when meta is a class (regardless of whether its own metaclass is type or some subclass of it). A non-type callable (e.g. a plain function) fails this check and is handled differently below.
  • metaclass is looked up by the interned identifier &_Py_ID(metaclass), so the key match is exact — you can't smuggle it through as e.g. a string with the same value.

2c. Default metaclass when metaclass= is absent

If no explicit metaclass is given, CPython synthesises one:

if (meta == NULL) {
    /* if there are no bases, use type: */
    if (PyTuple_GET_SIZE(bases) == 0) {
        meta = (PyObject *) (&PyType_Type);
    }
    /* else get the type of the first base */
    else {
        PyObject *base0 = PyTuple_GET_ITEM(bases, 0);
        meta = (PyObject *)Py_TYPE(base0);
    }
    Py_INCREF(meta);
    isclass = 1;  /* meta is really a class */
}

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L155-L167

Rules:

  • No bases → type.
  • Otherwise → type(bases[0]). Only the first base seeds the starting metaclass; the others participate through the conflict check below.

Because the default is always a real type, isclass is forced to 1 in this branch.

2d. Most-derived-metaclass resolution (only if the metaclass is a type)

When isclass is true (either defaulted, or user-supplied and passing isinstance(meta, type)), __build_class__ asks _PyType_CalculateMetaclass for the most-derived metatype that is compatible with every base:

if (isclass) {
    /* meta is really a class, so check for a more derived
       metaclass, or possible metaclass conflicts: */
    winner = (PyObject *)_PyType_CalculateMetaclass((PyTypeObject *)meta,
                                                    bases);
    if (winner == NULL) {
        goto error;
    }
    if (winner != meta) {
        Py_SETREF(meta, Py_NewRef(winner));
    }
}
/* else: meta is not a class, so we cannot do the metaclass
   calculation, so we will use the explicitly given object as it is */

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L169-L182

Critical consequence: if you pass a non-type callable as metaclass= (e.g. a function, or collections.namedtuple-style factory), CPython skips the derivation check entirely and uses your callable as-is. This is the officially-supported escape hatch for "metaclass as factory function".

The resolver itself is just a pairwise subtype walk — the explicit metaclass must be a (non-strict) subtype of every base's metatype, or some base's metatype must be a subtype of it (in which case that base's metatype "wins" and is used instead):

nbases = PyTuple_GET_SIZE(bases);
winner = metatype;
for (i = 0; i < nbases; i++) {
    tmp = PyTuple_GET_ITEM(bases, i);
    tmptype = Py_TYPE(tmp);
    if (PyType_IsSubtype(winner, tmptype))
        continue;
    if (PyType_IsSubtype(tmptype, winner)) {
        winner = tmptype;
        continue;
    }
    /* else: */
    PyErr_SetString(PyExc_TypeError,
                    "metaclass conflict: "
                    "the metaclass of a derived class "
                    "must be a (non-strict) subclass "
                    "of the metaclasses of all its bases");
    return NULL;
}
return winner;

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Objects/typeobject.c#L4117-L4151

This is where you get the familiar TypeError: metaclass conflict: ... for the classic "inherit from two classes with incompatible metaclasses" scenario.

2e. __prepare__, class-body execution, and metaclass call

Once a metaclass is chosen, __build_class__ asks it for a namespace via __prepare__(name, bases, **remaining_kwds). If the metaclass doesn't define __prepare__, a plain dict is used. The remaining keyword arguments (everything except metaclass) are forwarded:

if (PyObject_GetOptionalAttr(meta, &_Py_ID(__prepare__), &prep) < 0) {
    ns = NULL;
}
else if (prep == NULL) {
    ns = PyDict_New();
}
else {
    PyObject *pargs[2] = {name, bases};
    ns = PyObject_VectorcallDict(prep, pargs, 2, mkw);
    Py_DECREF(prep);
}

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L183-L193

The result is required to pass PyMapping_Check; otherwise a TypeError mentioning the metaclass name (or the literal string <metaclass> for non-type metaclasses) is raised:

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L197-L203

The class body closure is then executed with that mapping as its locals, after which the metaclass itself is called as meta(name, bases, ns, **remaining_kwds). If PEP 560 rewrote the bases, __orig_bases__ is stashed into the namespace first so the metaclass can see the un-rewritten tuple:

cell = _PyEval_Vector(tstate, (PyFunctionObject *)func, ns, NULL, 0, NULL);
if (cell != NULL) {
    if (bases != orig_bases) {
        if (PyMapping_SetItemString(ns, "__orig_bases__", orig_bases) < 0) {
            goto error;
        }
    }
    PyObject *margs[3] = {name, bases, ns};
    cls = PyObject_VectorcallDict(meta, margs, 3, mkw);

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L206-L214

Note that the metaclass receives the rewritten bases as its second positional argument, while __orig_bases__ carries the original tuple.

2f. __class__ cell consistency check

Finally, if any method in the body referenced __class__ (or used super() without arguments), the compiler will have produced a cell. __build_class__ cross-checks that the metaclass actually stored the freshly-built class into that cell — typically by type.__new__ honouring __classcell__ in the namespace. If a custom metaclass forgets to propagate __classcell__ you get the classic runtime error:

if (cls != NULL && PyType_Check(cls) && PyCell_Check(cell)) {
    PyObject *cell_cls = PyCell_GetRef((PyCellObject *)cell);
    if (cell_cls != cls) {
        if (cell_cls == NULL) {
            const char *msg =
                "__class__ not set defining %.200R as %.200R. "
                "Was __classcell__ propagated to type.__new__?";
            PyErr_Format(PyExc_RuntimeError, msg, name, cls);
        } else {
            const char *msg =
                "__class__ set to %.200R defining %.200R as %.200R";
            PyErr_Format(PyExc_TypeError, msg, cell_cls, name, cls);
        }

https://github.com/python/cpython/blob/d931725bc850cd096f6703bc285e885f1e015f05/Python/bltinmodule.c#L215-L227

This check only fires if cls is itself a type instance — non-type metaclasses are exempt, which is why a factory-function metaclass can return whatever it wants.

3. Summary of the observable semantics

Putting the above together, the rules a Python programmer actually sees:

  1. metaclass= is an ordinary class-statement keyword argument that the compiler passes through to __build_class__.
  2. __build_class__ pops it from the keyword dict before anything else looks at the kwargs, so __prepare__, __new__, and __init__ never see metaclass= — only the remaining keywords.
  3. If not supplied: defaults to type with zero bases, otherwise to type(bases[0]) — and always participates in the subtype check.
  4. If supplied and it's a type subclass: CPython picks the most-derived metaclass across the explicit value and type(b) for every base b. Pairs that are incomparable raise TypeError: metaclass conflict: ....
  5. If supplied and it's not a type (e.g. a function): it's used verbatim; no conflict check, no __classcell__ check.
  6. PEP 560 base rewriting (__mro_entries__) happens before metaclass resolution, so metaclass derivation is computed against the rewritten bases. The original tuple is re-exposed as __orig_bases__ on the namespace.
  7. The chosen metaclass's __prepare__ (if any) builds the namespace and receives the surviving keyword arguments; the class body runs against that namespace; then metaclass(name, bases, ns, **kwds) is called to produce the class object.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment