Skip to main content

funcobject.c: Python function object internals

Objects/funcobject.c defines PyFunction_Type and the full lifecycle of a Python function object from allocation through attribute access. It is one of the most-read files in CPython because every def statement ultimately lands here.

Map

LinesSymbolPurpose
1-60PyFunctionObject (struct layout)Canonical fields: func_code, func_globals, func_builtins, func_defaults, func_kwdefaults, func_closure, func_annotations, func_name, func_qualname
61-130PyFunction_NewWithQualNameAllocates and zero-initialises; borrows globals from frame
131-220func_get_code / func_set_codeGetter validates code.co_nfreevars matches closure length
221-310func_set_defaultsAccepts a tuple or None; clears on None
311-400func_get_annotationsLazy dict creation; returns empty dict rather than None in 3.14
401-500func_set_annotations3.14 splits storage into value annotations vs type-parameter annotations
501-620func_traverse / func_clearGC support; visits every borrowed and owned reference
621-720func_reprProduces <function qualname at 0x...>
721-800PyFunction_Type definitionSlots: tp_call delegates to _PyObject_Call, vectorcall set to _PyFunction_Vectorcall

Reading

PyFunctionObject struct fields

The nine fields live in Include/cpython/funcobject.h. The most important for the compiler are func_code (a PyCodeObject), func_globals (the module dict at definition time), and func_closure (a tuple of PyCell objects, one per free variable).

// CPython: Objects/funcobject.c:63 PyFunction_NewWithQualName
self->func_code = Py_NewRef(code);
self->func_globals = Py_NewRef(globals);
self->func_closure = NULL; // set later by MAKE_FUNCTION

func_defaults and func_kwdefaults are optional. They are NULL when the function has no defaults rather than an empty tuple, so callers must null-check before iterating.

func_get_code / func_set_code guard

Setting __code__ is allowed at runtime but CPython rejects any code object whose co_nfreevars count does not match the existing closure length. This prevents silent mismatches between cell indices and the closure tuple.

// CPython: Objects/funcobject.c:140 func_set_code
if (PySys_Audit("object.__setattr__", "OsO", func, "__code__", value) < 0)
return -1;
nclosure = (func->func_closure == NULL ? 0 :
PyTuple_GET_SIZE(func->func_closure));
nfree = ((PyCodeObject *)value)->co_nfreevars;
if (nclosure != nfree) {
PyErr_Format(PyExc_ValueError, ...);
return -1;
}

Annotation laziness in 3.14

Before 3.14, func_annotations held a plain dict. From 3.14 onward, CPython stores a compact representation and materialises the dict only on first access. func_get_annotations now calls _PyFunction_SetAnnotations which can hold either a dict (value annotations) or a NULL-sentinel tuple (type-parameter annotations from PEP 695). The getter always returns a dict, creating one from the compact form on demand.

// CPython: Objects/funcobject.c:348 func_get_annotations
if (func->func_annotations == NULL) {
func->func_annotations = PyDict_New();
if (func->func_annotations == NULL)
return NULL;
}
return Py_NewRef(func->func_annotations);

gopy notes

objects/function.go mirrors the struct with Go fields Code, Globals, Defaults, KwDefaults, and Closure. The annotation dict is stored eagerly (no lazy materialisation yet). The func_set_code closure-length guard is implemented in (*Function).SetCode and raises ValueError on mismatch, matching CPython behaviour.

The vectorcall path (_PyFunction_Vectorcall) is not yet ported; calls route through the slower tp_call equivalent. Tracking issue: task #481.