How CPython Implements code in C

The PyCodeObject struct, bytecode in co_code_adaptive, consts, names, localsplus kinds, cell/free vars, linetable, exception table, and code object creation traced through the CPython 3.15 source.

Python’s code type (types.CodeType) is the compiled representation of a function body, class body, or module: it holds the bytecode, constants, names, variable layout, source locations, and exception handling table. Functions bind one to globals and closure cells, but the code object itself is pure, shareable syntax. In CPython that is implemented by PyCodeObject in Objects/codeobject.c. This article traces the representation and the hot paths through the 3.15 source.

The object struct

PyCodeObject is a variable-size object: the trailing co_code_adaptive array holds the bytecode and is sized by PyObject_NewVar(..., size) where size is the number of _Py_CODEUNITs. The struct is defined via the _PyCode_DEF(SIZE) macro in Include/cpython/code.h:

c
// Include/cpython/code.h
struct PyCodeObject _PyCode_DEF(1);

// expands to:
typedef struct {
    PyObject_VAR_HEAD

    PyObject *co_consts;            /* tuple of constants used */
    PyObject *co_names;             /* tuple of strings (names used) */
    PyObject *co_exceptiontable;    /* bytes encoding exception handling */
    int co_flags;                   /* CO_..., see below */

    int co_argcount;                /* #arguments, except *args */
    int co_posonlyargcount;         /* #positional only arguments */
    int co_kwonlyargcount;          /* #keyword only arguments */
    int co_stacksize;               /* #entries needed for eval stack */
    int co_firstlineno;             /* first source line number */

    int co_nlocalsplus;             /* locals + cell + free slots */
    int co_framesize;               /* size of frame in words */
    int co_nlocals;                 /* number of local variables */
    int co_ncellvars;               /* total number of cell variables */
    int co_nfreevars;               /* number of free variables */
    uint32_t co_version;            /* version number */

    PyObject *co_localsplusnames;   /* tuple mapping offsets to names */
    PyObject *co_localspluskinds;   /* bytes: one kind byte per variable */
    PyObject *co_filename;          /* unicode (where loaded from) */
    PyObject *co_name;              /* unicode (name, for reference) */
    PyObject *co_qualname;          /* unicode (qualname, for reference) */
    PyObject *co_linetable;         /* bytes holding location info */
    PyObject *co_weakreflist;       /* weak references to code objects */
    _PyExecutorArray *co_executors; /* executors from optimizer */
    _PyCoCached *_co_cached;        /* cached co_* attributes */
    uintptr_t _co_instrumentation_version;
    struct _PyCoMonitoringData *_co_monitoring;
    Py_ssize_t _co_unique_id;      /* id for per-thread refcounting */
    int _co_firsttraceable;         /* index of first traceable insn */
    void *co_extra;                 /* scratch space for extra data */
    _PyCode_DEF_THREAD_LOCAL_BYTECODE()
    char co_code_adaptive[(SIZE)];  /* adaptive bytecode */
} PyCodeObject;

The “locals plus” arrays unify what used to be three separate tuples (co_varnames, co_cellvars, co_freevars). co_localsplusnames is a tuple of identifier strings and co_localspluskinds is a parallel bytes object whose entries are bitmasks of _PyLocals_Kind:

c
// Include/internal/pycore_code.h
#define CO_FAST_LOCAL   (0x20)   /* local variable */
#define CO_FAST_CELL    (0x40)   /* escapes into a closure */
#define CO_FAST_FREE    (0x80)   /* captured from an outer scope */
#define CO_FAST_ARG     (0x0E)   /* argument (pos/kw/var) */
#define CO_FAST_HIDDEN  (0x10)   /* hidden (LOAD_FAST_AND_CLEAR) */

The legacy names are still exposed lazily: PyCode_GetVarnames, PyCode_GetCellvars, and PyCode_GetFreevars filter co_localsplusnames by kind and cache the result in _co_cached.

Bytecode

The actual instructions live in co_code_adaptive, accessed through _PyCode_CODE(co) and sized by Py_SIZE(co) code units. co_code (the Python-visible attribute) is a derived bytes object rebuilt on demand by _PyCode_GetCode, which de-specializes adaptive entries back into the original opcodes:

c
// Objects/codeobject.c (_PyCode_GetCode)
PyObject *code = FT_ATOMIC_LOAD_PTR(cached->_co_code);
if (code != NULL) {
    return Py_NewRef(code);
}
...
code = PyBytes_FromStringAndSize((const char *)_PyCode_CODE(co),
                                  _PyCode_NBYTES(co));
deopt_code(co, (_Py_CODEUNIT *)PyBytes_AS_STRING(code));
FT_ATOMIC_STORE_PTR(cached->_co_code, code);

_PyCode_Quicken (called in init_code) specializes the raw bytecode into adaptive forms and inserts cache slots, so the eval loop can specialize at runtime. co_executors holds Tier-2 (uop) executors produced by the optimizer, and co_tlbc (free-threaded builds only) holds per-thread specialized copies.

Cached derived views and monitoring

Several attributes are derived and cached lazily in _co_cached (_PyCoCached): _co_code (the de-specialized bytes), _co_varnames, _co_cellvars, and _co_freevars. init_co_cached allocates this structure on first use, and access is guarded by a critical section plus atomic loads so it is safe under free-threading. Weak references are supported via co_weakreflist.

Code objects also carry _co_monitoring (per-tool/per-instruction monitoring data), _co_instrumentation_version, and co_executors for Tier 2. Lifecycle watchers registered with PyCode_AddWatcher receive PY_CODE_EVENT_CREATE (fired at the end of init_code) and PY_CODE_EVENT_DESTROY (fired in code_dealloc before teardown). co_extra provides indexed scratch slots whose free functions are registered per interpreter.

Frame size

co_framesize is co_nlocalsplus + co_stacksize + FRAME_SPECIALS_SIZE (computed in init_code), giving the frame’s total word size. The eval loop allocates a frame of this size, with localsplus slots first, then the evaluation stack, then special cells (return value, exception state, etc.). co_stacksize is an upper bound on operand-stack depth produced by the compiler.

consts, names, and flags

co_consts is a tuple: literals, nested code objects, and, when CO_HAS_DOCSTRING is set, the docstring as element 0. co_names is a tuple of strings used by LOAD_GLOBAL, LOAD_ATTR, etc. Constants and names are interned at construction time (intern_code_constants) so identity comparisons work in the eval loop.

co_flags combines function-kind flags (CO_OPTIMIZED, CO_NEWLOCALS, CO_VARARGS, CO_VARKEYWORDS, CO_NESTED, CO_GENERATOR, CO_COROUTINE, CO_ASYNC_GENERATOR) with compiler future flags and runtime hints such as CO_HAS_DOCSTRING and CO_METHOD.

Cell and free variables

There is no separate co_cellvars/co_freevars storage anymore; they are derived views over co_localsplus. A local that escapes into an inner function gets CO_FAST_CELL ORed into its kind (and may also be CO_FAST_LOCAL when it is both a local and a cell, e.g. a parameter captured by a closure). A variable from an enclosing scope is CO_FAST_FREE. The counts co_ncellvars and co_nfreevars are computed by get_localsplus_counts during validation.

At runtime, free variables are carried by PyCellObject boxes (see Include/cpython/cellobject.h); co_nfreevars tells the frame how many cell slots to copy from the function’s closure tuple.

linetable

co_linetable is a compact bytes object mapping bytecode offsets to source locations. Rather than one entry per instruction, it uses a variable-length encoding of “entries” keyed by _PyCodeLocationInfoKind:

c
// Include/cpython/code.h
typedef enum _PyCodeLocationInfoKind {
    PY_CODE_LOCATION_INFO_SHORT0 = 0,   /* short forms 0..9 */
    PY_CODE_LOCATION_INFO_ONE_LINE0 = 10,
    PY_CODE_LOCATION_INFO_ONE_LINE1 = 11,
    PY_CODE_LOCATION_INFO_ONE_LINE2 = 12,
    PY_CODE_LOCATION_INFO_NO_COLUMNS = 13,
    PY_CODE_LOCATION_INFO_LONG = 14,
    PY_CODE_LOCATION_INFO_NONE = 15
} _PyCodeLocationInfoKind;

PyCode_Addr2Line and PyCode_Addr2Location walk this table. When sys.flags/config disables debug ranges, remove_column_info rewrites entries to the NO_COLUMNS form at construction time, shrinking the table. The first traceable instruction index (_co_firsttraceable) is computed by scanning for the first RESUME so tracers skip module-level setup.

exception table

co_exceptiontable is a bytes object encoding the exception handling ranges as a sequence of varints: for each try block, a start offset, a length (end = start + length), a handler target offset, and a packed “depth/lasti” word whose high bits give the stack depth to restore and whose low bit records whether to push the current instruction index. All offsets are in code units (times two for byte offsets). _parse_exception_table in Lib/dis.py decodes it:

python
# Lib/dis.py
start = _parse_varint(iterator) * 2
length = _parse_varint(iterator) * 2
end = start + length
target = _parse_varint(iterator) * 2
dl = _parse_varint(iterator)
depth = dl >> 1
lasti = bool(dl & 1)

The eval loop consults this table when an exception propagates, replacing the old block-stack mechanism. An empty module-level code object uses the immortal empty bytes singleton.

code object creation

All construction goes through _PyCode_New, which takes a _PyCodeConstructor:

c
// Objects/codeobject.c
PyCodeObject *
_PyCode_New(struct _PyCodeConstructor *con)
{
    if (intern_code_constants(con) < 0) {
        return NULL;
    }
    // Compact linetable if debug ranges are disabled.
    if (!_Py_GetConfig()->code_debug_ranges) {
        replacement_locations = remove_column_info(con->linetable);
        con->linetable = replacement_locations;
    }

    Py_ssize_t size = PyBytes_GET_SIZE(con->code) / sizeof(_Py_CODEUNIT);
    PyCodeObject *co = PyObject_NewVar(PyCodeObject, &PyCode_Type, size);
    if (co == NULL) { ... }
    if (init_code(co, con) < 0) { Py_DECREF(co); return NULL; }
    return co;
}

init_code fills every field, derives co_nlocals/co_ncellvars/ co_nfreevars/co_framesize, assigns a fresh co_version from the interpreter’s func_state.next_version, copies the bytecode into co_code_adaptive, and calls _PyCode_Quicken to specialize it. It also fires code watchers (PY_CODE_EVENT_CREATE).

Before allocation, _PyCode_Validate checks argument types and ranges: code must be bytes aligned to _Py_CODEUNIT, consts/names/localsplusnames must be tuples, localspluskinds bytes of matching length, filename/name/ qualname unicode, linetable/exceptiontable bytes, and enough locals to cover arg counts. co_version is taken from interp->func_state.next_version under the func_state mutex, so each code object gets a unique version used by function specialization guards.

The public entry point PyUnstable_Code_NewWithPosOnlyArgs still accepts the legacy separate varnames/cellvars/freevars tuples and assembles them into the unified localsplus arrays, merging a cell that is also a local:

c
// Objects/codeobject.c (PyUnstable_Code_NewWithPosOnlyArgs)
for (i in cellvars) {
    // if the cell name is already a local, OR CO_FAST_CELL into
    // that local's kind instead of adding a new slot
    if (argoffset >= 0) {
        _PyLocals_SetKind(localspluskinds, argoffset, kind | CO_FAST_CELL);
        continue;
    }
    _Py_set_localsplus_info(offset, name, CO_FAST_CELL, ...);
}

PyCode_NewEmpty builds a minimal code object whose bytecode is a single RESUME followed by LOAD_COMMON_CONSTANT/RAISE_VARARGS (so calling it raises AssertionError), useful for stubs and tracebacks. code.__new__ (code_new_impl) validates inputs and delegates to PyCode_NewWithPosOnlyArgs.

Dealloc (code_dealloc) clears executors, decrements all owned objects (consts, names, localsplus arrays, filename, name, qualname, linetable, exceptiontable, cached views), frees monitoring data and (free-threaded) thread-local bytecode copies, and fires PY_CODE_EVENT_DESTROY.

Hashing and equality

code_richcompare compares co_name, the arg counts, co_flags, co_firstlineno, the bytecode (size and code units, cache slots included), consts (via _PyCode_ConstantKey), co_names, co_localsplusnames, co_linetable, and co_exceptiontable. It deliberately skips co_filename, co_qualname, co_stacksize, and co_version, so code objects from different files or with different stack sizes can still be equal when their executable content matches. code_hash mirrors these fields (plus Py_SIZE) using a fixed seed of 20221211.

Key takeaways

  • A code object is PyCodeObject: a PyObject_VAR_HEAD plus fields for consts, names, exception table, flags, arg counts, stack size, first line number, localsplus counts, version, localsplus names/kinds, filename/name/qualname, linetable, executors, cached views, monitoring data, and adaptive bytecode in the trailing array.
  • Bytecode is specialized in place (co_code_adaptive); co_code is a lazy, de-specialized bytes view.
  • Variable layout is unified into co_localsplusnames/co_localspluskinds; varnames/cellvars/freevars are filtered views.
  • Constants and names are interned; co_version drives function specialization.
  • Linetable and exception table are compact bytes encodings consulted by the eval loop and debuggers.
  • Creation funnels through _PyCode_New -> init_code, which validates, interns, derives counts, assigns a version, copies bytecode, and quickens it.

Further reading

  • Objects/codeobject.c – all of the above.
  • Include/cpython/code.h – PyCodeObject and public API.
  • Include/internal/pycore_code.h – _PyLocals_Kind and constructor.
  • Python/ceval.c – eval loop consumption of bytecode/exception table.
  • Python/compile.c – producer of _PyCodeConstructor.

PythonCpythonInternalsCode