CPython's super() Implementation in C: Cooperative Multiple Inheritance

super is an ordinary C type, PySuper_Type, that carries three pointers. Here's how CPython implements it: supercheck, MRO traversal in _PySuper_LookupDescr, descriptor binding, and the LOAD_SUPER_ATTR fast path.

TL;DR: super() isn’t a special syntax node. It’s an ordinary C type, PySuper_Type, holding three pointers: the class that called it, the object being searched, and the type whose MRO gets walked. And super().attr doesn’t even build one — LOAD_SUPER_ATTR calls straight into _PySuper_Lookup.

super() is the cooperative multiple-inheritance helper. Almost everything interesting — MRO traversal, descriptor binding, zero-argument super() — lives in Objects/typeobject.c.

The superobject struct

c
// Objects/typeobject.c
typedef struct {
    PyObject_HEAD
    PyTypeObject *type;      /* class invoking super()  (__thisclass__) */
    PyObject     *obj;       /* instance or class        (__self__)      */
    PyTypeObject *obj_type;  /* type of obj              (__self_class__) */
} superobject;

The three fields are exposed read-only through tp_members:

c
static PyMemberDef super_members[] = {
    {"__thisclass__",  _Py_T_OBJECT, offsetof(superobject, type),     Py_READONLY, ...},
    {"__self__",       _Py_T_OBJECT, offsetof(superobject, obj),      Py_READONLY, ...},
    {"__self_class__", _Py_T_OBJECT, offsetof(superobject, obj_type), Py_READONLY, ...},
    {0}
};

type is always a PyTypeObject *. obj may be NULL (unbound super(type) or class-level access) and obj_type then is NULL too.

And obj_type is the MRO we actually walk: for an instance it’s type(obj) (or obj.__class__); for a class argument it’s the class itself.

Lifecycle is ordinary GC-tracked:

c
static void super_dealloc(PyObject *self) {
    superobject *su = superobject_CAST(self);
    _PyObject_GC_UNTRACK(self);
    Py_XDECREF(su->obj);
    Py_XDECREF(su->type);
    Py_XDECREF(su->obj_type);
    Py_TYPE(self)->tp_free(self);
}

static int super_traverse(PyObject *self, visitproc visit, void *arg) {
    superobject *su = superobject_CAST(self);
    Py_VISIT(su->obj);
    Py_VISIT(su->type);
    Py_VISIT(su->obj_type);
    return 0;
}

Construction: super_init_impl

PySuper_Type.tp_new is PyType_GenericNew and tp_init is super_init, which delegates to super_init_impl. The argument forms are super(), super(type), and super(type, obj):

c
static inline int
super_init_impl(PyObject *self, PyTypeObject *type, PyObject *obj) {
    superobject *su = superobject_CAST(self);
    PyTypeObject *obj_type = NULL;
    if (type == NULL) {
        /* super() with no args: fill from __class__ and first local */
        PyThreadState *tstate = _PyThreadState_GET();
        _PyInterpreterFrame *frame = _PyThreadState_GetFrame(tstate);
        ...
        int res = super_init_without_args(frame, &type, &obj);
        ...
    }
    else {
        Py_INCREF(type);
        Py_XINCREF(obj);
    }
    if (obj == Py_None) { Py_DECREF(obj); obj = NULL; }
    if (obj != NULL) {
        obj_type = supercheck(type, obj);   /* validates + returns obj_type */
        ...
    }
    Py_XSETREF(su->type, (PyTypeObject*)type);
    Py_XSETREF(su->obj, obj);
    Py_XSETREF(su->obj_type, obj_type);
    return 0;
}

supercheck is the gatekeeper. It returns the type whose MRO we walk:

c
static PyTypeObject *
supercheck(PyTypeObject *type, PyObject *obj)
{
    /* class argument: super(type, cls) */
    if (PyType_Check(obj) && PyType_IsSubtype((PyTypeObject *)obj, type)) {
        return (PyTypeObject *)Py_NewRef(obj);
    }
    /* instance argument: super(type, instance) */
    if (PyType_IsSubtype(Py_TYPE(obj), type)) {
        return (PyTypeObject*)Py_NewRef(Py_TYPE(obj));
    }
    else {
        /* proxy case: obj.__class__ may differ from Py_TYPE(obj) */
        PyObject *class_attr;
        if (PyObject_GetOptionalAttr(obj, &_Py_ID(__class__), &class_attr) < 0)
            return NULL;
        if (class_attr != NULL && PyType_Check(class_attr) &&
            (PyTypeObject *)class_attr != Py_TYPE(obj)) {
            int ok = PyType_IsSubtype((PyTypeObject *)class_attr, type);
            if (ok) return (PyTypeObject *)class_attr;   /* transfers the GetOptionalAttr ref */
        }
        Py_XDECREF(class_attr);
    }
    ... raise TypeError ...
}

The proxy branch is what lets super() work with objects whose __class__ is overridden.

Zero-argument super()

When called as super() inside a method, CPython needs __class__ and the first positional argument. super_init_without_args pulls them out of the current frame: the first local (possibly a cell) is the instance, and the __class__ free variable is the class.

This is the machinery behind the implicit __class__ cell the compiler creates for methods:

c
static int super_init_without_args(_PyInterpreterFrame *cframe, ...) {
    PyObject *firstarg = ... locals array[0] ...;   /* instance or class */
    ...
    /* find the __class__ free var */
    for (; i < co->co_nlocalsplus; i++) {
        if (_PyUnicode_Equal(name, &_Py_ID(__class__))) {
            type = (PyTypeObject *) PyCell_GetRef(cell);
            ...
        }
    }
    *type_p = type;
    *obj_p = firstarg;
    return 0;
}

Worth internalizing: the compiler smuggles __class__ into the method’s closure, and super() reads it back out of the frame at runtime. That’s why zero-arg super() only works inside a method body, and why it breaks inside a nested function that isn’t a method.

super as a descriptor: super_descr_get

PySuper_Type.tp_descr_get = super_descr_get. So when super is stored in a class body and accessed on an instance, it binds to that instance:

c
static PyObject *
super_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
    superobject *su = superobject_CAST(self);
    if (obj == NULL || obj == Py_None || su->obj != NULL) {
        /* class-level access or already bound */
        return Py_NewRef(self);
    }
    if (!Py_IS_TYPE(su, &PySuper_Type))   /* subclass: delegate */
        return PyObject_CallFunctionObjArgs((PyObject *)Py_TYPE(su), su->type, obj, NULL);
    else {
        PyTypeObject *obj_type = supercheck(su->type, obj);
        ...
        newobj = (superobject *)PySuper_Type.tp_new(&PySuper_Type, NULL, NULL);
        newobj->type     = (PyTypeObject*)Py_NewRef(su->type);
        newobj->obj      = Py_NewRef(obj);
        newobj->obj_type = obj_type;
        return (PyObject *)newobj;
    }
}

That’s why super() inside a method can be used unbound at class scope and bound on instance access.

MRO traversal: _PySuper_LookupDescr

The core lookup skips everything up to and including su->type in the MRO of su->obj_type, then searches each following class’s __dict__:

c
PyObject *
_PySuper_LookupDescr(PyTypeObject *su_type, PyTypeObject *su_obj_type, PyObject *name)
{
    PyObject *mro = lookup_tp_mro(su_obj_type);   /* PyTuple of types */
    ...
    n = PyTuple_GET_SIZE(mro);
    for (i = 0; i+1 < n; i++) {
        if ((PyObject *)su_type == PyTuple_GET_ITEM(mro, i))
            break;
    }
    i++;   /* skip su->type */
    if (i >= n) return NULL;

    do {
        PyObject *obj  = PyTuple_GET_ITEM(mro, i);
        PyObject *dict = lookup_tp_dict(_PyType_CAST(obj));
        if (PyDict_GetItemRef(dict, name, &res) != 0) {
            /* found or error */
            return res;
        }
        i++;
    } while (i < n);
    return NULL;
}

Four details worth remembering:

  • It looks in tp_dict of each class, not via tp_getattro, so it does not re-trigger descriptors on the super object or __getattr__.
  • It uses PyDict_GetItemRef, which returns a strong reference and distinguishes “not found” (0) from “error” (-1).
  • The MRO is pinned with a C-stack reference because tp_mro can be replaced mid-lookup.
  • It returns the raw dict value. Descriptor binding happens in the caller.

Attribute lookup: super_getattro and do_super_lookup

super_getattro is PySuper_Type.tp_getattro. It special-cases __class__ (so super().__class__ returns super, not the instance’s class) and otherwise calls do_super_lookup:

c
static PyObject *
super_getattro(PyObject *self, PyObject *name)
{
    superobject *su = superobject_CAST(self);
    if (PyUnicode_Check(name) && PyUnicode_GET_LENGTH(name) == 9 &&
        _PyUnicode_Equal(name, &_Py_ID(__class__)))
        return PyObject_GenericGetAttr(self, name);

    return do_super_lookup(su, su->type, su->obj, su->obj_type, name, NULL);
}

do_super_lookup is where the descriptor binding happens:

c
static PyObject *
do_super_lookup(superobject *su, PyTypeObject *su_type, PyObject *su_obj,
                PyTypeObject *su_obj_type, PyObject *name, int *method)
{
    PyObject *res;
    int temp_su = 0;

    if (su_obj_type == NULL) goto skip;

    res = _PySuper_LookupDescr(su_type, su_obj_type, name);
    if (res != NULL) {
        if (method && _PyType_HasFeature(Py_TYPE(res), Py_TPFLAGS_METHOD_DESCRIPTOR)) {
            *method = 1;            /* method descriptor: caller handles binding */
        }
        else {
            descrgetfunc f = Py_TYPE(res)->tp_descr_get;
            if (f != NULL) {
                PyObject *res2 = f(res,
                    /* pass obj only in instance mode */
                    (su_obj == (PyObject *)su_obj_type) ? NULL : su_obj,
                    (PyObject *)su_obj_type);
                Py_SETREF(res, res2);
            }
        }
        return res;
    }
    else if (PyErr_Occurred()) return NULL;

skip:
    /* Fallback: build a temporary super() and use generic getattr */
    if (su == NULL) { ... su = (superobject *)PyObject_Vectorcall(...); temp_su=1; }
    res = PyObject_GenericGetAttr((PyObject *)su, name);
    if (temp_su) Py_DECREF(su);
    return res;
}

Two things to notice:

  1. When the found object’s type has tp_descr_get, it’s invoked with (descriptor, instance_or_NULL, su_obj_type). For class-mode super (super(type, cls)), su_obj == su_obj_type, so obj is passed as NULL — the descriptor runs in class-level mode. For instance mode, the instance is passed, so functions become bound methods.
  2. Method descriptors (Py_TPFLAGS_METHOD_DESCRIPTOR) are returned raw and flagged via *method, letting the bytecode layer avoid a second descriptor call and push self alongside the attribute.

The skip fallback covers the unbound case (super(type) with no object) and any path where no dict entry is found, by constructing a temporary super object and running generic attribute lookup (which can hit __getattr__).

The C API and bytecode fast path

_PySuper_Lookup wraps the whole thing for callers that don’t hold a superobject:

c
PyObject *
_PySuper_Lookup(PyTypeObject *su_type, PyObject *su_obj, PyObject *name, int *method)
{
    PyTypeObject *su_obj_type = supercheck(su_type, su_obj);
    if (su_obj_type == NULL) return NULL;
    PyObject *res = do_super_lookup(NULL, su_type, su_obj, su_obj_type, name, method);
    Py_DECREF(su_obj_type);
    return res;
}

The compiler lowers super().attr / super().meth(...) to LOAD_SUPER_ATTR. The specialized LOAD_SUPER_ATTR_ATTR and LOAD_SUPER_ATTR_METHOD forms call _PySuper_Lookup directly with the class and self on the stack, skipping construction of a super object entirely:

c
// Python/bytecodes.c
inst(LOAD_SUPER_ATTR_ATTR, ...) {
    ...
    PyObject *attr = _PySuper_Lookup((PyTypeObject *)class, self, name, NULL);
    ...
}
op(_LOAD_SUPER_ATTR_METHOD, ...) {
    ...
    attr_o = _PySuper_Lookup(cls, self, name,
        Py_TYPE(self)->tp_getattro == PyObject_GenericGetAttr ? method_found_ptr : NULL);
    if (method_found) {
        self_or_null = self_st;   /* push self so caller can call attr(self, ...) */
    }
    ...
}

The real win: for the common super().method() call, CPython allocates no super object at all. It goes straight from the class and self on the stack to the bound descriptor.

The JIT optimizer also uses _PySuper_LookupDescr directly (via lookup_super_attr) to cache the found descriptor when it’s a method descriptor, which is what makes super().method() in hot loops fast.

Key takeaways

  • super is PySuper_Type, a GC-tracked type with superobject (type, obj, obj_type) and read-only __thisclass__/__self__/__self_class__.
  • supercheck validates the object and returns the MRO-bearing type, including a proxy-friendly __class__ fallback.
  • Zero-arg super() extracts __class__ and the first local from the current frame.
  • super_descr_get binds an unbound super to an instance by cloning the struct.
  • MRO traversal in _PySuper_LookupDescr starts after su->type and scans each later class’s tp_dict with PyDict_GetItemRef.
  • do_super_lookup invokes tp_descr_get on the found object, passing the instance only in instance mode, and flags method descriptors.
  • LOAD_SUPER_ATTR bypasses super object construction entirely via _PySuper_Lookup.

Further reading

  • Objects/typeobject.c – superobject, super_init_impl, supercheck, super_descr_get, _PySuper_LookupDescr, do_super_lookup, super_getattro.
  • Include/internal/pycore_typeobject.h – _PySuper_Lookup, _PySuper_LookupDescr.
  • Python/bytecodes.c – LOAD_SUPER_ATTR, LOAD_SUPER_ATTR_ATTR, LOAD_SUPER_ATTR_METHOD.
  • Python/optimizer_analysis.c – lookup_super_attr.
  • Lib/test/test_super.py – behavior and edge cases.

PythonCpythonInternalsSuperMro