Function Objects and Code Objects

A function object (PyFunctionObject in C, the type types.FunctionType in Python) is the mutable, runtime thing that the def and lambda statements produce. It is a thin wrapper that pairs an immutable code object — the compiled bytecode and its metadata — with the context that a code object deliberately omits: the module globals to resolve names against, the default argument values, and the closure cells. CPython’s own header states the split bluntly: “Function objects are created by the execution of the ‘def’ statement. They reference a code object in their __code__ attribute, which is a purely syntactic object… There is one code object per source code ‘fragment’, but each code object can be referenced by zero or many function objects depending only on how many times the ‘def’ statement… was executed so far” (per Include/cpython/funcobject.h). The function object is where mutability and environment live; the code object is where the frozen instructions live. Understanding which state hangs off which object explains a surprising amount of Python’s behavior, from closures to default-argument late binding to method dispatch.

This note covers the function-object layer only. The fields of the code object it wraps (co_code, co_consts, co_varnames, co_flags, and friends) are documented separately in Code Objects and are not re-explained here.

Mental Model: A Wrapper Around a Frozen Template

The cleanest way to think about it: the code object is a read-only template, the function object is an instantiation of that template bound to an environment. The compiler emits exactly one code object for a given def in the source. Every time control flow reaches that def statement at runtime, CPython runs a MAKE_FUNCTION opcode that builds a fresh PyFunctionObject pointing at the same shared code object, but capturing whatever globals, defaults, and closure cells are in scope at that moment. Define a function inside a loop a thousand times and you get a thousand distinct function objects — but they may all share a single code object, because nothing about the compiled instructions changed from one definition to the next. Everything that can differ per-definition lives on the function object.

graph LR
    subgraph immutable["Immutable — shared, hashable, marshalable"]
        CO["PyCodeObject<br/>co_code · co_consts<br/>co_varnames · co_flags<br/>co_freevars · co_version"]
    end
    subgraph mutable["Mutable — one per def-execution"]
        FN["PyFunctionObject<br/>func_code → CO<br/>func_globals · func_builtins<br/>func_defaults · func_kwdefaults<br/>func_closure (tuple of cells)<br/>func_dict · func_annotate<br/>func_version"]
    end
    DEF["def / lambda<br/>(MAKE_FUNCTION at runtime)"] -->|"builds, wraps"| FN
    FN -->|"func_code"| CO
    FN -.->|"a call pushes a"| FRAME["PyInterpreterFrame<br/>(localsplus laid out<br/>per co_* metadata)"]
    CO -.->|"layout dictates"| FRAME

Figure: The function object (right) wraps the immutable code object (left), supplying the runtime context the code object refuses to hold — globals, defaults, closure cells. A call instantiates a frame whose local slots are laid out by the code object’s metadata but whose values come from the function object’s defaults and the call arguments. The insight to extract: mutability and environment belong to the function object so the code object can stay immutable and reusable, which is exactly what lets the same code object be shared across thousands of closures and cached in a .pyc.

The PyFunctionObject Struct, Field by Field

The C layout, verified against Include/cpython/funcobject.h at the v3.14.5 tag, begins with the standard PyObject_HEAD (the refcount and type pointer every object carries) followed by a block of fields shared with PyFrameConstructor via the _Py_COMMON_FIELDS macro, then function-specific fields:

typedef struct {
    PyObject_HEAD
    _Py_COMMON_FIELDS(func_)   // globals, builtins, name, qualname,
                               // code, defaults, kwdefaults, closure
    PyObject *func_doc;         /* The __doc__ attribute, can be anything */
    PyObject *func_dict;        /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;      /* The __module__ attribute, can be anything */
    PyObject *func_annotations; /* Annotations, a dict or NULL */
    PyObject *func_annotate;    /* Callable to fill the annotations dictionary */
    PyObject *func_typeparams;  /* Tuple of active type variables or NULL */
    vectorcallfunc vectorcall;
    uint32_t func_version;      /* Version number for use by specializer */
} PyFunctionObject;

Each field surfaces in Python as a dunder attribute. Walking them, with the documented semantics from the data model reference:

  • func_globals__globals__ — a reference (not a copy) to the dictionary that holds the global namespace of the module in which the function was defined. This is read-only from Python. Because it is a live reference, a function reads whatever the module’s globals contain at call time, not at definition time — which is why monkey-patching a module-level name affects functions already defined in it. The defining module’s namespace is itself a [[Module Objects and Namespaces|module object’s __dict__]].
  • func_builtins__builtins__ — the builtins namespace dict, cached on the function so name resolution does not have to walk to __globals__['__builtins__'] on every miss. Added in 3.10.
  • func_name / func_qualname__name__ / __qualname__ — the simple name ("greet") and the dotted qualified name ("Outer.greet" or "outer.<locals>.greet"). Both writable; the qualified name is what tracebacks and repr() display.
  • func_code__code__ — the code object. Writable, but assigning a code object with an incompatible number of free variables raises ValueError. Swapping __code__ is how some libraries hot-patch a function body in place.
  • func_defaults__defaults__None, or a tuple of default values for the trailing positional-or-keyword parameters that have defaults. Critically these live here, on the function object, not in the code object’s co_consts. They are evaluated once, in the enclosing scope, when MAKE_FUNCTION runs — the mechanism behind the classic mutable-default gotcha (see Default Arguments and Late Binding).
  • func_kwdefaults__kwdefaults__ — a dict of defaults for keyword-only parameters (those after a bare * or *args), or None.
  • func_closure__closure__None, or a tuple of cell objects, one per free variable named in the code object’s co_freevars. A cell is a tiny boxed reference (cell.cell_contents) shared between the enclosing function and the nested one; this is the entire machinery of closures, covered in Cell Variables and Closures. The header records the invariant: PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code).
  • func_dict__dict__ — the per-function attribute namespace, lazily created. This is why you can write f.custom_attr = 1 on a function; the assignment lands here. Built-in functions do not have this.
  • func_doc__doc__ — the docstring, or None.
  • func_module__module__ — the name of the defining module ("__main__", "os.path", …).
  • func_annotations__annotations__ and func_annotate__annotate__ — the annotations machinery, reworked for 3.14 (below).
  • func_typeparams__type_params__ — a tuple of TypeVar/ParamSpec/TypeVarTuple objects for PEP 695 generic functions (def f[T](x: T)). Added in 3.12.
  • vectorcall — the C function pointer that implements the fast vectorcall calling convention; for ordinary Python functions it points at _PyFunction_Vectorcall.
  • func_version — the specializer version counter (its own section below).

Note what is absent: there is no per-call state here. Argument values, local variables, and the evaluation stack live on the frame created for each call, not on the function object — which is why the same function object can be called re-entrantly and recursively without clobbering itself.

How def Builds One: MAKE_FUNCTION at Runtime

A def statement is not a declaration processed once by the compiler; it is executable code. When the compiler reaches a def, it emits bytecode that, when run, constructs the function object. The core opcode is MAKE_FUNCTION, verified against Python/bytecodes.c at v3.14.5:

inst(MAKE_FUNCTION, (codeobj_st -- func)) {
    PyObject *codeobj = PyStackRef_AsPyObjectBorrow(codeobj_st);
    PyFunctionObject *func_obj = (PyFunctionObject *)
        PyFunction_New(codeobj, GLOBALS());          // (1)
    PyStackRef_CLOSE(codeobj_st);
    ERROR_IF(func_obj == NULL);
    _PyFunction_SetVersion(                            // (2)
        func_obj, ((PyCodeObject *)codeobj)->co_version);
    func = PyStackRef_FromPyObjectSteal((PyObject *)func_obj);
}

Line by line: MAKE_FUNCTION consumes a single stack input — the code object that the compiler already placed in co_consts and loaded with LOAD_CONST. (1) It calls PyFunction_New(codeobj, GLOBALS()), where GLOBALS() is the currently executing frame’s globals dict. That single argument is how the function captures its defining module’s namespace: whatever module is running this def supplies func_globals. (2) It then stamps the function’s func_version from the code object’s co_version (see the version section). Notice what MAKE_FUNCTION does not take: no defaults, no closure, no annotations. Those are attached by separate follow-up opcodes.

When a function needs defaults, keyword-defaults, a closure, or annotations, the compiler emits SET_FUNCTION_ATTRIBUTE opcodes after MAKE_FUNCTION, one per attribute present. The handler:

inst(SET_FUNCTION_ATTRIBUTE, (attr_st, func_in -- func_out)) {
    PyObject *func = PyStackRef_AsPyObjectBorrow(func_in);
    PyObject *attr = PyStackRef_AsPyObjectSteal(attr_st);
    func_out = func_in;
    size_t offset = _Py_FunctionAttributeOffsets[oparg];   // (1)
    PyObject **ptr = (PyObject **)(((char *)func) + offset);
    assert(*ptr == NULL);
    *ptr = attr;                                            // (2)
    /* ... special-case fix-up of __annotate__ qualname ... */
}

The oparg selects which attribute from a small flag set defined in pycore_opcode_utils.h: MAKE_FUNCTION_DEFAULTS (0x01), MAKE_FUNCTION_KWDEFAULTS (0x02), MAKE_FUNCTION_ANNOTATIONS (0x04), MAKE_FUNCTION_CLOSURE (0x08), MAKE_FUNCTION_ANNOTATE (0x10). (1) _Py_FunctionAttributeOffsets[oparg] converts the flag into the byte offset of the right struct field, and (2) writes the prepared value (a tuple of defaults, a dict of kwdefaults, a tuple of cells for the closure, etc.) directly into it. The value was assembled by preceding bytecode — for defaults, BUILD_TUPLE over the evaluated default expressions; for a closure, LOAD_CLOSURE/BUILD_TUPLE over the enclosing cells. This staging is exactly why default expressions are evaluated at definition time in the enclosing scope and frozen onto the function object once: the tuple is built right there, then attached.

Crucially, def and lambda compile to the identical sequence. A lambda is just an expression that produces a function object via the same MAKE_FUNCTION (+ optional SET_FUNCTION_ATTRIBUTE) path; the only difference is that a lambda’s code object has a synthesized name ("<lambda>") and its body is a single implicit return. There is no separate “lambda object” type — type(lambda: 0) is types.FunctionType.

Deferred Annotations: PEP 649 / PEP 749 (New in 3.14)

Before 3.14, __annotations__ was a dict built eagerly when the function was defined, with every annotation expression evaluated immediately — which made forward references (def f(x: NotYetDefined)) require string quoting or from __future__ import annotations. Python 3.14 changes the default to lazy evaluation via PEP 649, as amended by PEP 749: per What’s New in 3.14, “annotations on functions, classes, and modules are no longer evaluated eagerly. Instead, annotations are stored in special-purpose annotate functions and evaluated only when necessary (except if from __future__ import annotations is used).”

The mechanism is the new func_annotate field (__annotate__). Instead of computing the annotations dict at def time, the compiler synthesizes a small annotate function — its own code object — that, when called, evaluates the annotation expressions and returns the dict. SET_FUNCTION_ATTRIBUTE with MAKE_FUNCTION_ANNOTATE attaches it. The __annotations__ dict is then computed lazily on first access: reading f.__annotations__ calls f.__annotate__(VALUE) and caches the result into func_annotations. The standard library annotationlib module (new in 3.14) lets callers request annotations in VALUE, FORWARDREF, or STRING formats by calling the annotate function with the corresponding format argument, so a tool can retrieve annotations as source strings without ever evaluating a name that does not exist yet. The SET_FUNCTION_ATTRIBUTE handler even has a special case (gh-137814) to fix the annotate function’s __qualname__ to <owner>.__annotate__.

The division of labor between the two PEPs is now pinned. PEP 649 (Larry Hastings) introduced the lazy-evaluation model; PEP 749 (Jelle Zijlstra, gh-119180) is the implementation as shipped and describes itself as supplementary — “This PEP supplements rather than supersedes PEP 649” (PEP 749). PEP 749 is the source of the concrete surface that landed in 3.14.5: the __annotate__ attribute name, the new annotationlib module, and the rule that the future import composes with the new machinery rather than bypassing it — “If the future import is active, the __annotate__ function of objects with annotations will return the annotations as strings when called with the VALUE format, reflecting the behavior of __annotations__.” All of this is verifiable on a live 3.14.5: annotationlib imports, exposes Format.VALUE, Format.FORWARDREF, and Format.STRING, and provides get_annotations, call_annotate_function, and ForwardRef. The full deferred-annotation story — formats, ForwardRef, the PEP 563 deprecation path — lives in Deferred Annotations.

The Function Version Cache: func_version

func_version is a 32-bit counter that exists purely to let the specializing (adaptive) interpreter cheaply prove that a call target has not changed since it was specialized. When a CALL instruction specializes — say to CALL_PY_EXACT_ARGS, the fast path for “call this exact Python function with the exact right number of positional args” — it records the callee’s func_version in its inline cache. On every subsequent execution it guards with a single integer comparison (_CHECK_FUNCTION_VERSION): if the live func_version still matches the cached one, the specialization’s assumptions (the code object, the defaults arity) still hold and the fast path runs; if not, the instruction de-optimizes back to the generic CALL.

The header enumerates exactly what resets the version to zero — the events that could invalidate a specialization (per funcobject.h):

Will be set to zero if any of these change:
    defaults
    kwdefaults (only if the object changes, not the contents of the dict)
    code
    annotations
    vectorcall function pointer

MAKE_FUNCTION seeds the version from the code object via _PyFunction_SetVersion, verified in Objects/funcobject.c:

void
_PyFunction_SetVersion(PyFunctionObject *func, uint32_t version)
{
    assert(func->func_version == FUNC_VERSION_UNSET);
    func->func_version = version;
#ifndef Py_GIL_DISABLED
    PyInterpreterState *interp = _PyInterpreterState_GET();
    struct _func_version_cache_item *slot = get_cache_item(interp, version);
    slot->func = func;                 // (1)
    slot->code = func->func_code;
#endif
}

(1) Besides setting the field, in the default (GIL-enabled) build it also writes the function and its code into a per-interpreter version cache indexed by version % FUNC_VERSION_CACHE_SIZE. The Tier 2 optimizer uses this side table to look up a function purely by its version number when building traces. When a guarded attribute changes, func_clear_version evicts the cache slot and sets func_version to a sentinel, instantly invalidating every specialized call site that guarded on it — no need to find them individually, because they will simply fail their next version check. (The free-threaded Py_GIL_DISABLED build omits the shared cache, since a process-wide table would need locking.)

The practical takeaway: reassigning f.__defaults__, f.__code__, f.__kwdefaults__ (the object, not a mutation of its contents), or f.__annotations__ is cheap-looking but quietly de-optimizes every call site that had specialized on that function. For hot code, mutating a function object in place is a performance footgun precisely because of this version invalidation.

Worked Example: Inspecting the Split

import dis
 
def make_adder(n=10):              # n=10 is a default → stored on the function
    def add(x):                    # add closes over n → free variable
        return x + n
    return add
 
f = make_adder(5)
g = make_adder(5)
 
f.__defaults__      # None — add() itself has no defaults
make_adder.__defaults__   # (10,) — lives on the function, NOT in co_consts
f.__closure__       # (<cell at ...: int object at ...>,)
f.__closure__[0].cell_contents   # 5
f.__code__ is g.__code__         # True  — same compiled body, shared code object
f is g                           # False — two distinct function objects
f.__globals__ is make_adder.__globals__   # True — same module namespace

Reading this against the mechanism: make_adder’s default 10 was evaluated and frozen onto make_adder.__defaults__ when its def ran at import. Each call to make_adder runs the inner def add → a MAKE_FUNCTION builds a fresh add function object whose func_closure is a one-tuple holding a cell boxing n. Because f and g come from two separate executions of the inner def, they are distinct objects — but f.__code__ is g.__code__ is True, the concrete proof that the immutable code object is shared while the function objects are not. Running dis.dis(make_adder) shows MAKE_FUNCTION followed by SET_FUNCTION_ATTRIBUTE 8 (the MAKE_FUNCTION_CLOSURE flag) for the inner add, and the default 10 attached to make_adder itself via the analogous SET_FUNCTION_ATTRIBUTE 1. The default 10 never appears in add’s nor make_adder’s co_consts as a “default” — it is a constant loaded to build the defaults tuple, which then lives on the function object.

Common Misunderstandings

“The code object holds the defaults / globals / closure.” No — that is the single most common confusion, and the entire reason the two object types exist. The code object is context-free and immutable; the data model spells it out: “the default argument values are stored in the function object, not in the code object… Unlike function objects, code objects are immutable and contain no references (directly or indirectly) to mutable objects” (data model). If defaults lived in the code object, the code object could not be immutable or hashable, and could not be safely shared across closures or marshalled into a .pyc.

“Each def in a loop recompiles the body.” No. The body is compiled once into one code object embedded in the enclosing code object’s co_consts. The loop re-runs only MAKE_FUNCTION, an allocation + field-wiring, not the compiler.

lambda is a different kind of object.” No — def and lambda produce the same types.FunctionType via the same opcodes; a lambda merely has a <lambda> name and a single-expression body.

“Setting f.__defaults__ is free.” It is cheap in cycles but de-optimizes specialized call sites by clearing func_version, as above.

Alternatives and Neighbors

A function object is one of several callables. A C-implemented function is a builtin_function_or_method (PyCFunctionObject), which has no __code__, __globals__, or writable __dict__ — it wraps a C function pointer instead of a code object. A class is callable (calling it runs __call__ on its metaclass). An instance of a class defining __call__ is callable. A bound method (types.MethodType) wraps a function object plus a __self__. Generators and coroutines are produced by calling functions whose code objects carry the CO_GENERATOR/CO_COROUTINE flags — same function object, different call result. The function object is specifically the Python-defined, code-object-backed callable, and it is the one the interpreter most aggressively optimizes (the entire CALL_PY_* specialization family targets it).

See Also