How CPython Implements Bound Methods (types.MethodType) in C

method (types.MethodType) as a GC-tracked object holding im_func/im_self, PyMethod_New, the 20-slot freelist, method_vectorcall prepending self, and the descriptor protocol (func_descr_get, cm_descr_get, sm_descr_get) traced through the CPython 3.15 source.

Python’s bound method – the object you get from obj.method and what types.MethodType names – is implemented in CPython in Objects/classobject.c as a small GC-tracked struct holding two references: the callable that implements the method (im_func, exposed as __func__) and the instance it is bound to (im_self, exposed as __self__). The same file also keeps the older instancemethod helper type. This article traces that implementation through the 3.15 source.

The object: PyMethodObject

The layout is revealed (non-limited API only) in Include/cpython/classobject.h:

c
// Include/cpython/classobject.h
typedef struct {
    PyObject_HEAD
    PyObject *im_func;        /* The callable object implementing the method */
    PyObject *im_self;        /* The instance it is bound to */
    PyObject *im_weakreflist; /* List of weak references */
    vectorcallfunc vectorcall;
} PyMethodObject;

Invariants:

  • im_func and im_self are never NULL in a live object. im_self is always a real instance: PyMethod_New rejects NULL/None, and method.__new__ enforces the same.
  • im_func may be any callable, not just a function – method.__new__ only checks PyCallable_Check. This is what makes types.MethodType(callable, obj) a general binding tool.
  • PyMethod_Check(op) is an exact type check (Py_IS_TYPE((op), &PyMethod_Type)), so subclasses are not methods for C-level fast paths.
  • The object is GC-tracked (Py_TPFLAGS_HAVE_GC) because it owns two references, and supports vectorcall (Py_TPFLAGS_HAVE_VECTORCALL) and weak references (tp_weaklistoffset).

The C-level accessors are PyMethod_Function/PyMethod_Self (with refcount-neutral PyMethod_GET_FUNCTION/PyMethod_GET_SELF macros):

c
// Objects/classobject.c
PyObject *
PyMethod_Function(PyObject *im)
{
    if (!PyMethod_Check(im)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    return ((PyMethodObject *)im)->im_func;
}

PyObject *
PyMethod_Self(PyObject *im)
{
    if (!PyMethod_Check(im)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    return ((PyMethodObject *)im)->im_self;
}

Construction: PyMethod_New and the freelist

c
// Objects/classobject.c
PyObject *
PyMethod_New(PyObject *func, PyObject *self)
{
    if (self == NULL) {
        PyErr_BadInternalCall();
        return NULL;
    }
    PyMethodObject *im = _Py_FREELIST_POP(PyMethodObject, pymethodobjects);
    if (im == NULL) {
        im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
        if (im == NULL) {
            return NULL;
        }
    }
    im->im_weakreflist = NULL;
    im->im_func = Py_NewRef(func);
    im->im_self = Py_NewRef(self);
    im->vectorcall = method_vectorcall;
    _PyObject_GC_TRACK(im);
    return (PyObject *)im;
}

Notes:

  • func and self are both re-referenced (not consumed) via Py_NewRef. Callers keep their own references.
  • The freelist holds up to Py_pymethodobjects_MAXFREELIST (20) entries, so repeatedly binding/unbinding methods in hot code reuses slots instead of hitting the allocator.
  • im_weakreflist is reset to NULL before tracking, and method_dealloc clears weakrefs first.

The Python-facing constructor is method.__new__ (clinic-generated method_new):

c
// Objects/classobject.c
static PyObject *
method_new_impl(PyTypeObject *type, PyObject *function, PyObject *instance)
{
    if (!PyCallable_Check(function)) {
        PyErr_SetString(PyExc_TypeError,
                        "first argument must be callable");
        return NULL;
    }
    if (instance == NULL || instance == Py_None) {
        PyErr_SetString(PyExc_TypeError,
            "instance must not be None");
        return NULL;
    }
    return PyMethod_New(function, instance);
}

So types.MethodType(callable, obj) requires a callable and a non-None instance; passing None raises TypeError.

Calling a bound method: method_vectorcall

A bound method is called by prepending im_self to the argument vector and delegating to the underlying callable:

c
// Objects/classobject.c
static PyObject *
method_vectorcall(PyObject *method, PyObject *const *args,
                  size_t nargsf, PyObject *kwnames)
{
    assert(Py_IS_TYPE(method, &PyMethod_Type));

    PyThreadState *tstate = _PyThreadState_GET();
    PyObject *self = PyMethod_GET_SELF(method);
    PyObject *func = PyMethod_GET_FUNCTION(method);
    return _PyObject_VectorcallPrepend(tstate, func, self, args, nargsf, kwnames);
}

_PyObject_VectorcallPrepend (in Objects/call.c) either mutates the vector in place when PY_VECTORCALL_ARGUMENTS_OFFSET is set (fast path) or allocates a small stack and copies, placing self at index 0 and shifting the caller’s arguments. The result is that obj.method(a, b) becomes func(obj, a, b).

PyMethod_Type.tp_call is PyVectorcall_Call, and tp_vectorcall_offset points at the vectorcall field, so the vectorcall fast path is used wherever possible.

The descriptor protocol: where binding happens

Binding is not performed by method itself. method_descr_get simply returns the same object:

c
// Objects/classobject.c
static PyObject *
method_descr_get(PyObject *meth, PyObject *obj, PyObject *cls)
{
    Py_INCREF(meth);
    return meth;
}

Binding happens in the tp_descr_get of the objects stored in class dictionaries. The relevant ones in Objects/funcobject.c:

Plain function: func_descr_get

c
// Objects/funcobject.c
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{
    if (obj == Py_None || obj == NULL) {
        return Py_NewRef(func);        // unbound access: ClassName.method
    }
    return PyMethod_New(func, obj);    // bound access: obj.method
}

This is the heart of the “unbound vs bound” story. In Python 2, accessing ClassName.method returned an unbound method (a method object with no instance). In Python 3 that was removed: when obj is NULL (class-level access) or None, the function is returned unchanged, so there is no separate “unbound method” type. Only instance access produces a method.

classmethod: cm_descr_get

c
// Objects/funcobject.c
static PyObject *
cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
    classmethod *cm = (classmethod *)self;
    if (type == NULL)
        type = (PyObject *)(Py_TYPE(obj));
    return PyMethod_New(cm->cm_callable, type);
}

A classmethod is always bound – to the class (type), never to an instance. Even obj.method binds to the class.

staticmethod: sm_descr_get

c
// Objects/funcobject.c
static PyObject *
sm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
    staticmethod *sm = (staticmethod *)self;
    return Py_NewRef(sm->sm_callable);
}

A staticmethod is never bound: it returns the underlying callable unchanged, so no self/cls is injected.

instancemethod descriptor

PyInstanceMethod_Type (same file) wraps a callable in a descriptor whose __get__ binds on instance access:

c
// Objects/classobject.c
static PyObject *
instancemethod_descr_get(PyObject *descr, PyObject *obj, PyObject *type) {
    PyObject *func = PyInstanceMethod_GET_FUNCTION(descr);
    if (obj == NULL) {
        return Py_NewRef(func);     // class access: return function
    }
    else
        return PyMethod_New(func, obj);
}

PyInstanceMethod_New is the C API for creating such descriptors directly; it is rarely used inside CPython itself but is exposed for extension modules. Its __new__ is instancemethod_new.

Attribute resolution: method_getattro

A bound method forwards unknown attribute lookups to its underlying callable, but lets the method’s own attributes (and the type’s) take precedence:

c
// Objects/classobject.c
static PyObject *
method_getattro(PyObject *obj, PyObject *name)
{
    PyMethodObject *im = (PyMethodObject *)obj;
    PyTypeObject *tp = Py_TYPE(obj);
    PyObject *descr = NULL;

    {
        if (!_PyType_IsReady(tp)) {
            if (PyType_Ready(tp) < 0)
                return NULL;
        }
        descr = _PyType_LookupRef(tp, name);
    }

    if (descr != NULL) {
        descrgetfunc f = TP_DESCR_GET(Py_TYPE(descr));
        if (f != NULL) {
            PyObject *res = f(descr, obj, (PyObject *)Py_TYPE(obj));
            Py_DECREF(descr);
            return res;
        }
        else {
            return descr;
        }
    }

    return PyObject_GetAttr(im->im_func, name);
}

So obj.method.__doc__ resolves to method_get_doc, which in turn reads __doc__ from im_func; other attributes (e.g. __wrapped__) fall through to the underlying function. tp_setattro is PyObject_GenericSetAttr, so attributes can be set on the instance dict when present.

Equality, hashing, repr

  • method_richcompare compares im_func with PyObject_RichCompareBool and then pointer-compares im_self; only ==/!= are supported.
  • method_hash mixes PyObject_GenericHash(im_self) with PyObject_Hash(im_func).
  • method_repr formats <bound method NAME of OBJ> using __qualname__/__name__ of im_func.
  • method___reduce__ rebuilds via getattr(self, name) (with name mangling for private names), so bound methods round-trip through pickle as getattr(obj, name).

GC, deallocation, and freelist return

c
// Objects/classobject.c
static void
method_dealloc(PyObject *self)
{
    PyMethodObject *im = _PyMethodObject_CAST(self);
    _PyObject_GC_UNTRACK(im);
    FT_CLEAR_WEAKREFS(self, im->im_weakreflist);
    Py_DECREF(im->im_func);
    Py_XDECREF(im->im_self);
    assert(Py_IS_TYPE(self, &PyMethod_Type));
    _Py_FREELIST_FREE(pymethodobjects, (PyObject *)im, PyObject_GC_Del);
}

static int
method_traverse(PyObject *self, visitproc visit, void *arg)
{
    PyMethodObject *im = _PyMethodObject_CAST(self);
    Py_VISIT(im->im_func);
    Py_VISIT(im->im_self);
    return 0;
}

method_traverse visits both owned references; tp_clear is NULL (the freelist-backed objects are not cleared individually). After unlinking weakrefs and dropping the two references, the slot goes back to the 20-entry freelist via _Py_FREELIST_FREE.

The companion instancemethod

PyInstanceMethod_Type (“instancemethod”) is a thinner wrapper around a single func pointer. It is GC-tracked, has no freelist, and its __get__ binds on instance access just like a function. It exists mainly as a C-API building block (PyInstanceMethod_New) for code that wants to store a method-like descriptor in a class dict without going through a Python function. Its tp_call calls the wrapped callable directly without prepending anything.

Memory layout summary

text
PyMethodObject
+---------------------------+
| ob_refcnt / ob_tid        |
+---------------------------+
| *ob_type  (&PyMethod_Type)|
+---------------------------+
| *im_func  (never NULL)    |  -> any callable
+---------------------------+
| *im_self  (never NULL)    |  -> instance (never None)
+---------------------------+
| *im_weakreflist           |
+---------------------------+
| vectorcall                 |  -> method_vectorcall
+---------------------------+

Fixed size (sizeof(PyMethodObject)), GC-tracked, with a 20-slot freelist. The two payload pointers are the entire state: binding is just storing self alongside func, and calling prepends self.

Key takeaways

  • types.MethodType is PyMethod_Type, a GC-tracked struct holding im_func (__func__) and im_self (__self__); im_self is never None.
  • PyMethod_New re-references both fields and reuses a 20-slot freelist.
  • Calling a bound method runs method_vectorcall, which prepends im_self to the argument vector via _PyObject_VectorcallPrepend.
  • Binding itself happens in descriptors’ tp_descr_get: func_descr_get produces a method only on instance access (class access returns the function unchanged), cm_descr_get binds to the class, and sm_descr_get binds nothing.
  • There is no “unbound method” in Python 3: ClassName.method is just the function.
  • method_getattro lets the method’s own/type attributes win, then forwards to im_func.
  • PyInstanceMethod_Type is the descriptor-only variant used by the C API.

Further reading

  • Objects/classobject.c – PyMethodObject, PyMethod_New, method_vectorcall, method_getattro, dealloc/traverse, instance method type.
  • Include/cpython/classobject.h – PyMethodObject layout and PyMethod_GET_* accessors.
  • Objects/funcobject.c – func_descr_get, cm_descr_get, sm_descr_get (where binding actually occurs).
  • Objects/call.c – _PyObject_VectorcallPrepend.
  • Lib/types.py – MethodType = type(_C()._m).
  • Lib/test/test_types.py and Lib/test/test_descr.py – reference behavior and descriptor edge cases.

PythonCpythonInternalsMethod