Code Objects

A code object (PyCodeObject in C, exposed as the built-in code type) is the compiled, executable unit of Python: a frozen package of bytecode plus all the metadata the interpreter needs to run it — the constants it references, the names it touches, the number and kind of its local variables, the source-location table, and the exception-handling table. Every function body, every class body, every module, and every generator expression compiles to its own code object; the compiler emits them, the evaluation loop executes them, and marshal serializes them into [[Bytecode Caching and pyc Files|.pyc files]]. Crucially, a code object carries no runtime context — no globals, no default argument values, no closure cells. That separation is exactly what distinguishes it from a function object, which wraps a code object together with that context. The Python documentation states it plainly: “the function object contains an explicit reference to the function’s globals … also 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” (per the data model reference).

Mental Model

Think of a code object as a read-only template and a function object as an instantiation of that template bound to an environment. The same code object can back many function objects: define a function inside a loop a thousand times and you get a thousand PyFunctionObjects, but they may all share one code object, because the code object is the part that does not change from one definition to the next. The things that do change per definition — the globals dictionary, the default values, the closure cells — live on the function object instead.

graph TD
    SRC["Source: def f(x): return x + 1"] -->|"compiler emits"| CO["PyCodeObject<br/>co_code_adaptive · co_consts · co_names<br/>co_localsplusnames · co_flags · co_linetable<br/>co_exceptiontable · co_argcount …"]
    CO -->|"wrapped by MAKE_FUNCTION"| FN["PyFunctionObject<br/>__code__ → CO<br/>__globals__ · __defaults__ · __closure__"]
    CO -->|"marshalled to disk"| PYC["name.cpython-314.pyc"]
    FN -->|"call → new frame"| FRAME["Frame<br/>fast-locals array · value stack<br/>(borrows layout from CO)"]
    CO -.->|"executed against"| FRAME

Figure: A code object is the immutable, context-free compiled artifact (centre). The function object (right) wraps it with the mutable runtime context — globals, defaults, closure — and a call instantiates a frame whose local-variable slots are laid out exactly as the code object’s metadata dictates. The same code object can be marshalled to a .pyc (top right) and reused across processes. The insight: nothing context-dependent is baked into the code object, which is precisely why it can be immutable, hashable, and cached on disk.

The C Struct, Field by Field

The C definition lives in Include/cpython/code.h, built from a macro _PyCode_DEF(SIZE) so the same field list can be reused by the deep-freeze tooling. Reading it directly is the only way to get the 3.14 layout right, because the public Python attributes you see from inspect are partly reconstructed from internal fields rather than stored verbatim. The struct begins with PyObject_VAR_HEAD — it is a variable-length object, because the bytecode itself is stored inline at the tail of the struct (see co_code_adaptive below). The fields, grouped as the source groups them:

The hot fields (touched every instruction in the eval loop), placed first for cache locality:

  • PyObject *co_consts — a tuple of the literal constants the bytecode loads (numbers, strings, nested code objects, None, the docstring if present). The comment in the header calls it “list (constants used)” but it is materialised as a tuple. When a LOAD_CONST executes, its oparg indexes this tuple.
  • PyObject *co_names — a tuple of strings: the global and attribute names the bytecode references (e.g. print, len, the .append in x.append). These are names looked up by dictionary, in contrast to fast locals.
  • PyObject *co_exceptiontable — a bytes object encoding the zero-cost exception table (see below); maps each bytecode range to the handler that covers it.
  • int co_flags — the bitfield of CO_* flags (see below).

The cooler descriptive fields:

  • int co_argcount — total positional parameters, including positional-only ones and ones with defaults, but excluding *args. For def f(a, b=2, /, c) this is 3.
  • int co_posonlyargcount — how many of those are positional-only (before the /). New since 3.8.
  • int co_kwonlyargcount — keyword-only parameters (those after a bare * or after *args).
  • int co_stacksize — the maximum depth the value stack will ever reach while executing this code. The frame allocator uses it to size the stack region; getting it wrong would corrupt memory, so the compiler computes it conservatively by abstract interpretation.
  • int co_firstlineno — the source line number of the first line of the construct.

Redundant counts derived from the locals-plus arrays (cached so the eval loop need not recompute them):

  • int co_nlocalsplus — total slots for local + cell + free variables.
  • int co_framesize — size of the frame in machine words.
  • int co_nlocals — number of plain local variables (parameters included).
  • int co_ncellvars — number of cell variables (locals captured by an inner scope; see Cell Variables and Closures).
  • int co_nfreevars — number of free variables (names this code closes over from an enclosing scope).
  • uint32_t co_version — a monotonically increasing version stamp bumped whenever the function’s identity-relevant state changes; the specializing interpreter uses it to invalidate inline caches.

The unified name/kind tables (the big 3.11+ change):

  • PyObject *co_localsplusnamesone tuple naming every local, cell, and free variable in a single offset space.
  • PyObject *co_localspluskinds — a parallel bytes object, one byte per variable, tagging each as local / cell / free / and so on.

This unification is why the familiar co_varnames, co_cellvars, and co_freevars are no longer stored as three separate tuples. They are reconstructed on demand from co_localsplusnames + co_localspluskinds and memoised in a small cache struct, _PyCoCached:

typedef struct {
    PyObject *_co_code;       // de-adapted "clean" bytecode, lazily built
    PyObject *_co_varnames;   // reconstructed local-variable names
    PyObject *_co_cellvars;   // reconstructed cell-variable names
    PyObject *_co_freevars;   // reconstructed free-variable names
} _PyCoCached;

So when you read code.co_varnames you are not reading a stored field — you are triggering a lazy reconstruction (_PyCode_GetVarnames in Objects/codeobject.c) that filters co_localsplusnames by kind and caches the result.

The reference/identity fields:

  • PyObject *co_filename — the path the code was compiled from.
  • PyObject *co_name — the simple name (f, <module>, <lambda>, <genexpr>).
  • PyObject *co_qualname — the dotted qualified name (Outer.method.<locals>.inner), new in 3.11.
  • PyObject *co_linetable — the PEP 626 source-location table (see below).
  • PyObject *co_weakreflist — supports weak references to the code object.

The runtime/optimization fields (mutable, excluded from hashing):

  • _PyExecutorArray *co_executors — pointers to compiled JIT / tier-2 executors produced by the optimizer.
  • _PyCoCached *_co_cached — the lazy reconstruction cache shown above.
  • uintptr_t _co_instrumentation_version, struct _PyCoMonitoringData *_co_monitoring — bookkeeping for sys.monitoring / sys.settrace.
  • void *co_extra — opaque scratch space for tools (the _PyEval_RequestCodeExtraIndex API).

The trailing inline bytecode — the field most often described wrong:

    char co_code_adaptive[(SIZE)];   // the actual, mutable, specializable bytecode

Since 3.11 the bytecode is not a separate bytes object hanging off a pointer — it is an inline char array at the very end of the struct, which is why PyCodeObject is a variable-length object. The internal docs spell out the motivation: “this was changed to save an allocation and to allow it to be mutated” (per InternalDocs/code_objects.md). It must be mutable because the specializing adaptive interpreter (PEP 659) rewrites generic opcodes into type-specialized ones in place in this array, and weaves inline cache entries between instructions.

Do not confuse co_code_adaptive (the struct field) with the public co_code attribute — the difference is load-bearing. There is no co_code field on the 3.14 struct; the only stored bytecode is the inline co_code_adaptive array. The public code.co_code is a read-only property that returns the de-adapted “clean” bytecode — the original, un-specialized instructions with inline caches stripped out — lazily rebuilt and cached in _PyCoCached._co_code so that disassembly and marshal see stable bytes regardless of how the live co_code_adaptive has been quickened in place.

Reading the actual routine in v3.14.5 Objects/codeobject.c confirms the mechanism. The co_code property calls _PyCode_GetCode (line 2310), which first returns the memoised _co_cached->_co_code if it exists; otherwise, under a per-object critical section, it copies the raw co_code_adaptive bytes into a fresh bytes object (PyBytes_FromStringAndSize(_PyCode_CODE(co), _PyCode_NBYTES(co))) and hands that buffer to deopt_code (line 2294) to clean in place, then atomically stores it in _co_code. deopt_code walks the instructions one code unit at a time: for each, it fetches the base (un-specialized) opcode via _Py_GetBaseCodeUnit(code, i) — asserting inst.op.code < MIN_SPECIALIZED_OPCODE, i.e. the result is always a generic opcode — writes that base instruction back, then for the caches = _PyOpcode_Caches[inst.op.code] inline-cache code units that follow it, zeroes each (instructions[i+j].cache = 0) and skips past them (i += caches). The net effect is exactly the “clean” view: specialized opcodes reverted to their generic forms and every inline-cache slot blanked to zero, so disassembly and marshal see stable, version-portable bytes.

The co_flags Bitfield

co_flags is a single int whose bits answer structural questions about the code object cheaply. The exact masks are defined in code.h; the human-readable semantics are documented under “Code Objects Bit Flags” in the inspect module. Walking the important ones:

  • CO_OPTIMIZED (0x0001) — “The code object is optimized, using fast locals.” Set for ordinary functions: locals live in the frame’s fast-locals array indexed by LOAD_FAST/STORE_FAST (see Fast Locals and the LOAD_FAST Family), not in a dictionary. Module and class bodies do not have it.
  • CO_NEWLOCALS (0x0002) — “If set, a new dict will be created for the frame’s f_locals when the code object is executed.” Functions get a fresh local namespace per call; class bodies do too, but module bodies reuse the module dict. In practice CO_OPTIMIZED | CO_NEWLOCALS (0x3) is the signature of a function — and you can verify it: (lambda: 0).__code__.co_flags & 0x3 == 0x3, while a module’s co_flags is 0x0.
  • CO_VARARGS (0x0004) — has a *args-style variable positional parameter.
  • CO_VARKEYWORDS (0x0008) — has a **kwargs-style variable keyword parameter.
  • CO_NESTED (0x0010) — the code object is a nested function (defined inside another function).
  • CO_GENERATOR (0x0020) — executing the code returns a generator object rather than running the body (see Generators and the yield Mechanism). def g(): yield has co_flags & 0x20.
  • CO_COROUTINE (0x0080) — an async def coroutine function; executing it returns a coroutine object.
  • CO_ITERABLE_COROUTINE (0x0100) — marks a generator-based coroutine (the legacy @types.coroutine path) so it may be awaited.
  • CO_ASYNC_GENERATOR (0x0200) — an async def containing yield — returns an async generator.
  • CO_HAS_DOCSTRING (0x4000000) — new in 3.14 — set when the source had a docstring; if so, the docstring is the first item of co_consts. This replaces the old “always reserve co_consts[0] for the docstring” convention with an explicit flag.
  • CO_METHOD (0x8000000) — new in 3.14 — set when the function was defined in class scope (a method, structurally).
  • The high bits (CO_FUTURE_DIVISION = 0x20000 and up) record active from __future__ import … directives, so the right syntax/semantics are applied; these are kept above 0x10000 to avoid colliding with the PyCF_ compile-flag space (per the comment in code.h, changed in 3.9 by bpo-39562).

Source Locations: the PEP 626 Line Table

co_linetable is the compact, opaque table mapping each bytecode instruction back to its source position. Despite the name, the internal docs note it “includes more than line numbers; it represents a 4-number source location for every instruction, indicating the precise line and column at which it begins and ends” (per code_objects.md). The public window onto it is code.co_positions(), which “returns an iterable over the source code positions of each bytecode instruction,” yielding (start_line, end_line, start_column, end_column) tuples where columns are 0-indexed UTF-8 byte offsets (per the data model reference). That four-number precision is what lets a 3.11+ traceback underline the exact subexpression that raised, e.g. pointing a caret at the specific a[i] in a chained expression.

PEP 626 is the contract this table fulfils: “line tracing events are generated for all lines of code executed and only for lines of code that are executed,” and “the f_lineno attribute of frame objects should always contain the expected line number.” The lower-fidelity co_lines() iterator ((start, end, lineno) byte-offset ranges) is the PEP 626 line-only view; it is what debuggers use to fire line events. The format is a sequence of variable-length entries, each beginning with a byte whose most-significant bit is set, followed by zero or more continuation bytes with that bit clear — a self-delimiting encoding tuned for compactness, since this metadata is large and rarely consulted on the hot path.

The “Format of the locations table” section of code_objects.md pins the exact byte layout. The leading byte of every entry packs three fields: bit 7 is the always-set marker (1), bits 3–6 hold a 4-bit code selecting the entry form, and bits 0–2 hold (length in code units − 1) — so one entry covers 1–8 instructions. The code then dictates how the start line, end line, start column, and end column are derived from the following bytes:

  • Codes 0–9 — short form (two bytes total): start/end line deltas are 0; the code itself encodes the start column as code*8, and the second byte refines it — start_column = (code*8) + ((second_byte>>4)&7), end_column = start_column + (second_byte&15).
  • Codes 10–12 — one-line form: start-line delta is code − 10 (0, 1, or 2), end-line delta is 0; start and end columns are each one unsigned byte.
  • Code 13 — no column info: start-line delta is a signed varint (svarint), end-line delta 0, columns None.
  • Code 14 — long form: start-line delta is an svarint, end-line delta a varint, start and end columns each a varint.
  • Code 15 — no location: line and columns all None.

The deltas are relative: the start line is a delta from the previous entry’s start line (or from co_firstlineno for the first entry), and the end line is a delta from that same entry’s start line. This is why the table cannot be random-accessed — PyCode_Addr2Line / _PyCode_LocationFromAddr must scan from the top accumulating deltas, which is acceptable precisely because the table is only consulted on tracebacks and tracing, not on the execution hot path.

The legacy co_lnotab attribute (start-line-only, pre-3.10 format) still exists but is deprecated since 3.12 and may be removed in 3.15 (per the data model docs); it is lazily synthesised from co_linetable when accessed. Don’t reach for it in new code.

The Zero-Cost Exception Table

co_exceptiontable is the other side table that makes 3.11+ fast. Before 3.11, every try block pushed and popped block-stack bookkeeping at runtime even when no exception ever fired — paying for try on the happy path. The Zero-Cost Exception Handling redesign moved all of that into co_exceptiontable: a bytes object mapping each range of bytecode offsets to the handler that should run if an exception propagates out of that range, plus the stack depth to restore. When no exception occurs, nothing in this table is ever read, so try is genuinely free when not triggered; only on an actual raise does the unwinder binary-search the table to find the covering handler. See Zero-Cost Exception Handling for the full mechanism and the table’s encoding.

Immutability, Hashing, and Identity

Code objects are “nominally immutable” — the public attributes can’t be assigned, and the object contains no references to mutable objects, which makes it safe to share, hash, and marshal. But “nominally” is doing work: as the internal docs note, “Some fields (including co_code_adaptive and fields for runtime information such as _co_monitoring) are mutable, but mutable fields are not included when code objects are hashed or compared.” The header’s comment enumerates the only fields that participate in hash/equality: co_name, co_argcount, co_posonlyargcount, co_kwonlyargcount, co_nlocals, co_stacksize, co_flags, co_firstlineno, co_consts, co_names, and co_localsplusnames. Name and line number are deliberately included so that constant de-duplication won’t collapse two identical lambdas defined on different lines into one — they must stay distinguishable for tracebacks and debuggers.

To “modify” a code object you create a copy: code.replace(**kwargs) (since 3.8) returns a new code object with selected fields swapped, and copy.replace(code, ...) works too. This is how tools like coverage and bytecode rewriters produce edited code without mutating the original.

One Code Object Per Scope — and the Comprehension Subtlety

Each lexical scope that produces bytecode gets its own code object: the module body (co_name == '<module>'), each function and lambda, each class body (it runs as code to populate the class namespace), and each generator expression ('<genexpr>'). You can see them nested as constants: a module’s co_consts contains the code objects of the functions defined at module level, whose co_consts in turn contain their nested functions.

The brief’s old rule “every comprehension is its own code object” is stale as of 3.12. PEP 709 inlined list, set, and dict comprehensions directly into their enclosing scope — they no longer create a separate code object or push a frame, which is why they run measurably faster. Generator expressions are the exception: because a genexp must produce a lazily-resumable generator object, it still compiles to its own <genexpr> code object. Verified on 3.14.5: [x for x in xs] inside a function produces no nested code object, while (x for x in xs) produces one named <genexpr>. State it precisely — list/set/dict comprehensions are inlined; generator expressions are not.

How a Code Object Comes to Be, and Where It Goes

The compiler is the producer: after the AST and symbol table are built, the compiler walks each scope, emits instructions, computes co_stacksize and the location/exception tables, and assembles the PyCodeObject. Notably, when a code object is created (including when read back from disk), _PyCode_Quicken() in Python/specialize.c runs to initialise the adaptive caches in co_code_adaptive, “because the on-disk format is a sequence of bytes, and some of the caches need to be initialized with 16-bit values” (per the internal docs).

The consumer is the evaluation loop: a call creates a frame whose fast-locals array and value stack are sized from co_nlocalsplus, co_framesize, and co_stacksize, then the loop fetches and dispatches co_code_adaptive instructions, indexing co_consts/co_names/the locals-plus space by oparg.

For persistence, the code object is serialized with marshal and written into a .pyc file so the next process can skip recompilation entirely — covered in the sibling note Bytecode Caching and pyc Files, which is where the marshal format and the .pyc header live.

Inspecting Code Objects in Practice

The outputs below are real, run on CPython 3.14.5:

def greet(name, /, greeting="hi", *args, **kw):
    """Say hello."""        # docstring → CO_HAS_DOCSTRING, co_consts[0]
    return f"{greeting}, {name}"
 
c = greet.__code__
c.co_argcount         # 2  (name + greeting; *args/**kw not counted)
c.co_posonlyargcount  # 1  (name, before the /)
c.co_kwonlyargcount   # 0
c.co_varnames         # ('name', 'greeting', 'args', 'kw')  ← reconstructed
c.co_consts           # ('Say hello.', ', ')  ← docstring, then the f-string literal
greet.__defaults__    # ('hi',)  ← NOT in co_consts: defaults live on the function
c.co_flags & 0x3      # 3  (CO_OPTIMIZED | CO_NEWLOCALS → it's a function)
bool(c.co_flags & 0x4000000)  # True (CO_HAS_DOCSTRING, 3.14)
list(c.co_positions())[:2]    # [(1,1,0,0), (3,3,14,22)] (line,endline,col,endcol)
c.replace(co_name="greet2").co_name  # 'greet2'; c.co_name still 'greet'

Each commented line maps directly to a struct field or its reconstruction discussed above. Note especially co_consts: the default value "hi" does not appear in it. Default arguments are evaluated in the enclosing scope at definition time and stored on the function object (greet.__defaults__), exactly the function-vs-code-object separation the opening blockquote described; inside greet, greeting is a fast local accessed by LOAD_FAST, never a constant. Use [[The dis Module and Bytecode Disassembly|dis.dis(greet)]] to see the bytecode in co_code (the de-adapted form) decoded against co_consts/co_names.

See Also