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:
// 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_funcandim_selfare never NULL in a live object.im_selfis always a real instance:PyMethod_NewrejectsNULL/None, andmethod.__new__enforces the same.im_funcmay be any callable, not just a function –method.__new__only checksPyCallable_Check. This is what makestypes.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):
// 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
// 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:
funcandselfare both re-referenced (not consumed) viaPy_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_weakreflistis reset toNULLbefore tracking, andmethod_deallocclears weakrefs first.
The Python-facing constructor is method.__new__ (clinic-generated
method_new):
// 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:
// 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:
// 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
// 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
// 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
// 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:
// 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:
// 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_richcomparecomparesim_funcwithPyObject_RichCompareBooland then pointer-comparesim_self; only==/!=are supported.method_hashmixesPyObject_GenericHash(im_self)withPyObject_Hash(im_func).method_reprformats<bound method NAME of OBJ>using__qualname__/__name__ofim_func.method___reduce__rebuilds viagetattr(self, name)(with name mangling for private names), so bound methods round-trip through pickle asgetattr(obj, name).
GC, deallocation, and freelist return
// 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
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.MethodTypeisPyMethod_Type, a GC-tracked struct holdingim_func(__func__) andim_self(__self__);im_selfis neverNone.PyMethod_Newre-references both fields and reuses a 20-slot freelist.- Calling a bound method runs
method_vectorcall, which prependsim_selfto the argument vector via_PyObject_VectorcallPrepend. - Binding itself happens in descriptors’
tp_descr_get:func_descr_getproduces amethodonly on instance access (class access returns the function unchanged),cm_descr_getbinds to the class, andsm_descr_getbinds nothing. - There is no “unbound method” in Python 3:
ClassName.methodis just the function. method_getattrolets the method’s own/type attributes win, then forwards toim_func.PyInstanceMethod_Typeis 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–PyMethodObjectlayout andPyMethod_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.pyandLib/test/test_descr.py– reference behavior and descriptor edge cases.