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
| File | Role |
|---|---|
Python/frame.c | Frame allocation, chunk allocator. |
Objects/frameobject.c | The Python-visible frame object. |
Include/internal/pycore_frame.h | _PyInterpreterFrame layout. |
Include/cpython/pyframe.h | The 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:
- Allocates a
PyGenObjectwhosegi_iframefield is the current frame storage. - Copies the live state of the interpreter frame into
gi_iframe. - 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.