Skip to main content

Frames

A frame is the execution context for one invocation of a code object. It holds the value stack, the locals, the link to its caller, and the program counter.

Source map

FileRole
Python/frame.cFrame allocation, chunk allocator.
Objects/frameobject.cThe Python-visible frame object.
Include/internal/pycore_frame.h_PyInterpreterFrame layout.
Include/cpython/pyframe.hThe public PyFrameObject type.

Two frame types

CPython distinguishes between the lightweight _PyInterpreterFrame that the eval loop uses, and the heavyweight PyFrameObject that Python code can introspect.

_PyInterpreterFrame is a struct, not a PyObject. It lives on a per-thread frame chunk that grows like a value stack. It has no refcount and no GC participation. The eval loop reads it directly and accesses fields by pointer.

PyFrameObject is a PyObject with a _PyInterpreterFrame * inside it. It is allocated lazily, when Python code calls sys._getframe, when the debugger requests it, or when a frame escapes via inspect.currentframe. Allocating the wrapper does not move the interpreter frame; the wrapper holds a pointer.

Layout

+--------------------------------+
| previous frame pointer | prev
| code object | f_code
| function object | f_func_obj
| globals | f_globals
| builtins | f_builtins
| locals | f_locals
| executable (Tier-2 trampoline) | f_executable
| return offset | return_offset
| program counter | prev_instr
| stacktop offset | stacktop
| owner (interpreter / generator)| owner
| localsplus[0..nlocalsplus] | fast locals + cells + frees
| stack[0..stacksize] | value stack (right after locals)
+--------------------------------+

localsplus is one contiguous slab. The first nlocals entries are the fast locals named in co_varnames. The next ncellvars are cell pointers; the next nfreevars are cell pointers captured from the enclosing scope. The slab is followed immediately by the value stack.

This packing is why LOAD_FAST and STORE_FAST are a single load or store from localsplus[oparg], and why LOAD_DEREF is one indirection (((PyCellObject *)localsplus[oparg])->ob_ref).

Allocation

Frames are allocated from a thread-local pool. Each pool entry is a 16 KiB chunk. _PyThreadState_PushFrame returns the next aligned slot in the current chunk, allocating a new chunk if needed. _PyThreadState_PopFrame rewinds the pointer.

This allocator is what makes Python's call cost so low. A function call is mostly an offset bump and a RESUME.

Linking

prev is the caller frame. Following the chain backward gives the Python call stack. sys._getframe(n) walks n prev links.

A frame's owner field distinguishes frames owned by the interpreter (regular calls) from frames owned by a generator, coroutine, or async generator. Generator-owned frames are stored in the generator object's gi_iframe slot rather than on the chunk, and survive across YIELD_VALUE.

Generator frames

When a function with the CO_GENERATOR flag is called, the eval loop reaches RETURN_GENERATOR. That opcode:

  1. Allocates a PyGenObject whose gi_iframe field is the current frame storage.
  2. Copies the live state of the interpreter frame into gi_iframe.
  3. Pops the frame and returns the generator to the caller.

On next(gen) / gen.send(v), the eval loop is re-entered with the generator's frame and throwflag = 0. The frame resumes exactly where it left off because prev_instr was preserved.

Tracebacks

Each entry in a traceback object holds a frame and a lasti (the byte offset of the instruction that failed). The frame's f_executable plus lasti is enough to reconstruct the source position via the co_linetable. The traceback module formats these chains.

Reading order

Continue to the Specializer for the adaptive layer that runs on top of dispatch, or Generators for the suspend / resume protocol.