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:
// 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:
// 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:
// 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:
// 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:
# 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:
// 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:
// 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: aPyObject_VAR_HEADplus 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_codeis 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_versiondrives 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–PyCodeObjectand public API.Include/internal/pycore_code.h–_PyLocals_Kindand constructor.Python/ceval.c– eval loop consumption of bytecode/exception table.Python/compile.c– producer of_PyCodeConstructor.