PyFrameObject and _PyInterpreterFrame layout, frame creation, the frame stack, and frame introspection.
A frame is the runtime record for one active execution of Python code. When CPython calls a Python function, executes a module body, runs a class body, resumes a generator, or resumes a coroutine, it uses a frame-like execution record to hold the current state.
A code object says what to execute.
A frame says where execution currently is and what values are currently live.
code object = immutable instructions and metadata
frame = mutable execution state for one run of that codeFor this function:
def add(a, b):
c = a + b
return cThe code object contains bytecode, constants, names, variable layout, stack size, flags, and source mapping information.
A frame created for add(2, 3) contains the current argument values, local variable slots, stack values, instruction position, exception state, and links to the execution context.
28.1 Why Frames Exist
A Python program can have many active calls at the same time:
def a():
return b()
def b():
return c()
def c():
return 42
a()During execution, CPython needs to remember each suspended caller while the callee runs.
Conceptually:
frame for a
waiting for b
frame for b
waiting for c
frame for c
currently executingEach frame stores enough state to resume its code after a nested call returns.
A frame answers these questions:
Which code object is running?
Which instruction is next?
What are the local variables?
What temporary values are on the stack?
Which globals and builtins are visible?
What exception is being handled?
Is tracing or profiling active?
Who called this frame?28.2 Code Object vs Frame
A code object is reusable. A frame is one execution instance.
def f(x):
return x + 1
a = f(10)
b = f(20)Both calls use the same code object:
print(f.__code__)But each call has its own frame state.
| Concept | Code object | Frame |
|---|---|---|
| Mutability | Mostly immutable | Mutable |
| Lifetime | Often long-lived | Usually one call or suspended execution |
| Contains bytecode | Yes | References code object |
| Contains locals | No | Yes |
| Contains stack | No | Yes |
| Contains instruction pointer | No | Yes |
| Shared across calls | Yes | No |
The code object is the program fragment. The frame is the running activation of that fragment.
28.3 Function Frames
A function call creates or initializes a frame for the callee.
def square(x):
return x * x
square(9)The function object contains:
code object
globals
defaults
keyword defaults
closure cells
annotations
qualname
module nameWhen called, CPython combines the function object with the actual arguments and creates a frame execution state.
Conceptually:
function object
code object
globals
call arguments
x = 9
new frame
code = square.__code__
globals = square.__globals__
locals slot 0 = 9
value stack = empty
instruction pointer = startThe evaluation loop then runs the frame until it returns, raises, yields, or suspends.
28.4 Module Frames
A module body is also executed in a frame.
For a file:
# app.py
x = 10
def f():
return xCPython compiles the whole module into a code object. Running the file executes that code object in a module namespace.
Conceptually:
module frame
code = compiled app.py
globals = module.__dict__
locals = module.__dict__At module scope, globals and locals usually refer to the same dictionary.
This is why top-level assignment writes into the module namespace:
x = 10creates:
module.__dict__["x"] = 10Function execution is different. Function locals use fast local slots, not the module dictionary as the primary storage.
28.5 Class Body Frames
Class bodies also execute as code.
class User:
kind = "human"
def name(self):
return "anonymous"The class statement does not simply declare a static structure. CPython executes the class body in a temporary namespace, then builds the class object from that namespace.
Conceptually:
prepare class namespace
execute class body frame
collect assignments and function objects
call metaclass to create class object
bind class object to name UserDuring class body execution:
kind = "human"stores into the class namespace.
The nested function:
def name(self):
return "anonymous"creates a function object and stores it in the class namespace. It does not run the method body.
After the class body finishes, CPython passes the namespace to the metaclass, usually type.
28.6 Frame Contents
A CPython frame-like execution record contains several categories of data.
| Category | Purpose |
|---|---|
| Code reference | The code object being executed |
| Globals | Global namespace for name lookup |
| Builtins | Builtin namespace fallback |
| Locals | Local variables or namespace |
| Fast locals | Slot array for function locals |
| Value stack | Temporary operands for bytecode |
| Instruction state | Current bytecode position |
| Exception state | Active exception handling metadata |
| Caller relation | Link to previous frame or call context |
| Tracing state | Debugger, profiler, coverage hooks |
| Owner state | Generator, coroutine, or normal call state |
A simplified structure looks like this:
frame
code object
previous frame
globals dictionary
builtins dictionary
locals representation
fast local slots
value stack
instruction pointer
exception state
tracing flagsThe exact C structs change across CPython versions. The conceptual fields remain stable.
28.7 Fast Locals
Function locals are optimized.
In this function:
def f(a, b):
c = a + b
return cThe compiler knows the local names:
a
b
cIt assigns them indexes:
| Name | Slot |
|---|---|
a | 0 |
b | 1 |
c | 2 |
During execution, CPython can access locals by slot:
LOAD_FAST 0
LOAD_FAST 1
STORE_FAST 2
LOAD_FAST 2This avoids dictionary lookup for ordinary function locals.
The frame stores these values in an array-like region:
fast locals
slot 0: a
slot 1: b
slot 2: cThis is one reason local variables are faster than globals.
28.8 Locals Dictionary
Python exposes locals through locals() and frame attributes.
def f(a):
b = a + 1
print(locals())
f(10)Output:
{'a': 10, 'b': 11}But inside a function, the actual execution storage is fast locals. The dictionary view may be materialized or synchronized with internal storage depending on context and Python version.
This distinction is important:
def f():
x = 1
locals()["x"] = 2
return xYou should not rely on modifying the dictionary returned by locals() to change optimized local variables inside a function.
At module scope, the situation differs:
locals()["x"] = 2
print(x)At module level, locals are the module dictionary, so this usually works.
28.9 Globals and Builtins
A frame stores references to the global and builtin namespaces used for name resolution.
For:
def f(xs):
return len(xs)len is not a local variable. CPython uses global lookup:
look in function globals
if missing, look in builtins
if missing, raise NameErrorThe frame provides both dictionaries:
frame.globals = module namespace
frame.builtins = builtins namespaceConceptually:
def f(xs):
return len(xs)runs as:
LOAD_GLOBAL len
LOAD_FAST xs
CALL 1
RETURN_VALUELOAD_GLOBAL depends on the frame’s globals and builtins.
28.10 Value Stack
Each frame has a value stack used by bytecode execution.
For:
def f(a, b, c):
return (a + b) * cThe stack changes roughly like this:
| Instruction | Stack before | Stack after |
|---|---|---|
LOAD_FAST a | [] | [a] |
LOAD_FAST b | [a] | [a, b] |
BINARY_OP + | [a, b] | [a + b] |
LOAD_FAST c | [a + b] | [a + b, c] |
BINARY_OP * | [a + b, c] | [(a + b) * c] |
RETURN_VALUE | [result] | return |
The stack stores object references, not raw unboxed values.
So for integers, the stack holds pointers to Python integer objects.
value stack
PyObject* -> int object
PyObject* -> int objectBytecode instructions push, pop, replace, or inspect these stack entries.
28.11 Instruction Position
A frame tracks the current bytecode position.
For straight-line code, the instruction pointer advances.
For branches, it jumps.
def abs_like(x):
if x < 0:
return -x
return xConceptual control flow:
load x
load 0
compare <
jump if false to return_x
load x
unary negative
return
return_x:
load x
returnThe frame records which instruction is next. This position matters for:
normal execution
branches
loops
exceptions
tracebacks
line tracing
profiling
debuggingTracebacks use frame state to report where an exception occurred.
28.12 Frames and Tracebacks
When an exception propagates, CPython records traceback information from frames.
Example:
def a():
b()
def b():
c()
def c():
1 / 0
a()The traceback shows the chain of active calls:
a
b
c
ZeroDivisionErrorEach traceback entry refers to a frame and instruction position. This is why Python can show source filenames, line numbers, and function names.
A traceback is not merely a string. It is a structured chain of runtime information.
28.13 Accessing Frames From Python
Python exposes frame objects through several APIs.
import inspect
def f():
frame = inspect.currentframe()
print(frame.f_code.co_name)
print(frame.f_locals)
print(frame.f_globals is globals())
f()The frame object exposes fields such as:
| Attribute | Meaning |
|---|---|
f_code | Code object |
f_locals | Local namespace view |
f_globals | Global namespace |
f_builtins | Builtins namespace |
f_back | Previous frame |
f_lineno | Current source line |
f_trace | Trace function |
You can also use:
import sys
frame = sys._getframe()
print(frame.f_code.co_name)These APIs are powerful but implementation-sensitive. Keeping frame objects alive can also keep local variables and objects alive.
28.14 Frame Lifetime
Most normal function frames are short-lived.
def f():
x = object()
return 1
f()When f returns, its frame can be destroyed or reused, and its local references can be released.
But frames may live longer in several cases:
tracebacks keep frames alive
generators suspend frames
coroutines suspend frames
debuggers inspect frames
profilers observe frames
closures may keep cells alive
manual references to frames keep them aliveExample:
import inspect
saved = None
def f():
global saved
x = [1, 2, 3]
saved = inspect.currentframe()
f()The global saved now refers to the frame. That frame refers to its locals. The list assigned to x may remain alive because the frame remains alive.
This is a common source of surprising memory retention in debugging tools and exception handling code.
28.15 Frame Chains
A frame can reference its caller through f_back.
import inspect
def outer():
inner()
def inner():
frame = inspect.currentframe()
print(frame.f_code.co_name)
print(frame.f_back.f_code.co_name)
outer()Conceptually:
inner frame
f_back -> outer frame
f_back -> module frameThis chain enables traceback construction and stack inspection.
It also means holding the innermost frame can keep the caller frames alive.
saved inner frame
keeps outer frame
keeps module frame referencesFor memory-sensitive tools, frame references must be released deliberately.
28.16 Generators and Suspended Frames
Generators keep execution state after yielding.
def gen():
x = 1
yield x
x = 2
yield xCalling the function creates a generator object:
g = gen()The body does not run immediately. When resumed:
next(g)the frame runs until the first yield.
After yield, the frame remains suspended:
generator object
suspended frame
code object
local x = 1
instruction position after first yield
value stack stateThe next call resumes from that saved instruction position:
next(g)This is different from a normal function frame, which ends on return.
28.17 Coroutines and Frames
Coroutines use the same general idea as generators: resumable execution state.
async def fetch():
data = await read()
return dataAt await, the coroutine can suspend. Its frame retains:
local variables
current instruction position
pending awaitable
exception state
return pathAn event loop later resumes it.
coroutine object
suspended frame or frame-like state
locals
stack
instruction pointerAsync execution therefore depends on frame suspension and resumption. It is not a separate language runtime.
28.18 Frame Materialization
Modern CPython distinguishes internal execution frames from full Python-visible frame objects.
The interpreter can run with compact internal frames for performance. A full frame object may be created only when needed by introspection, tracing, debugging, traceback handling, or APIs such as sys._getframe().
Conceptually:
internal frame
optimized execution record
Python frame object
object exposed to Python code
wraps or materializes execution stateThis distinction improves performance because not every function call needs a heap-allocated Python frame object visible to user code.
The conceptual model remains the same: there is still execution state. The exact representation can vary.
28.19 Frames and Closures
Closures require special storage for variables shared across nested functions.
Example:
def outer():
x = 10
def inner():
return x
return innerThe variable x must survive after outer returns because inner still uses it.
CPython handles this with cell objects.
Conceptually:
outer frame
x stored in cell
inner function
closure references same cellAfter outer returns, the frame can go away, but the cell remains alive because the returned function references it.
fn = outer()
print(fn())The closure does not keep the whole outer frame alive in the normal case. It keeps the needed cell variables alive.
28.20 Cell and Free Variables
The compiler classifies closure variables.
| Term | Meaning |
|---|---|
| Cell variable | Local variable captured by an inner function |
| Free variable | Variable used here but defined in an outer scope |
Example:
def outer():
x = 1
def inner():
return x
return innerFor outer, x is a cell variable.
For inner, x is a free variable.
You can inspect this:
def outer():
x = 1
def inner():
return x
return inner
print(outer.__code__.co_cellvars)
print(outer().__code__.co_freevars)The frame layout includes storage for these cells so nested functions can share variables safely.
28.21 Frames and Exceptions
Frames store exception handling state.
For:
def f(x):
try:
return 10 / x
except ZeroDivisionError:
return 0The code object contains exception handling metadata. The frame stores the current execution state needed to use that metadata.
When division raises an exception, the interpreter uses the frame’s instruction position to find the matching handler.
Conceptually:
exception raised
inspect current frame
locate handler for current bytecode range
adjust stack state
jump to handlerIf no handler exists in the current frame, the frame unwinds and the exception propagates to the caller frame.
28.22 Frames and finally
A finally block also depends on precise frame state.
def f():
try:
return 1
finally:
cleanup()The finally block runs even though the function is returning.
The frame must remember that a return is pending while it executes the cleanup code.
Conceptually:
start return with value 1
enter finally block
call cleanup
if cleanup succeeds:
complete original return
if cleanup raises:
replace return with new exceptionThis is why exception and return state are part of frame execution, not simple afterthoughts.
28.23 Frames and Tracing
Tracing hooks operate on frames.
import sys
def trace(frame, event, arg):
print(event, frame.f_code.co_name, frame.f_lineno)
return trace
def f(x):
y = x + 1
return y
sys.settrace(trace)
f(10)
sys.settrace(None)The trace function receives the current frame. It can inspect code, locals, globals, line numbers, and call relationships.
Tracing is useful for:
debuggers
coverage tools
teaching tools
profilers
runtime monitorsBut tracing has a cost. It forces the interpreter to preserve and expose more state at more execution points.
28.24 Frames and Profiling
Profiling is similar but usually coarser than tracing.
import sys
def profile(frame, event, arg):
print(event, frame.f_code.co_name)
sys.setprofile(profile)
def f():
return 1
f()
sys.setprofile(None)Profiling events include calls and returns. Profilers use frame data to attribute time or call counts to functions.
The frame provides the mapping from runtime execution to source-level program structure.
28.25 Frames and Recursion
Recursive calls create multiple frames for the same code object.
def fact(n):
if n <= 1:
return 1
return n * fact(n - 1)For fact(4):
fact(4)
fact(3)
fact(2)
fact(1)Each call has its own local n.
frame fact: n = 4
frame fact: n = 3
frame fact: n = 2
frame fact: n = 1All frames reference the same code object, but their local slots differ.
CPython tracks recursion depth to prevent unbounded recursive calls from exhausting lower-level resources.
28.26 Frames and Memory Retention
Frames can retain more memory than expected.
Example:
def load_big():
data = bytearray(100_000_000)
raise RuntimeError("failed")
try:
load_big()
except RuntimeError as exc:
saved = excThe exception can retain a traceback. The traceback can retain frames. The frames can retain local variables. Therefore data may remain alive.
A common cleanup pattern is:
try:
load_big()
except RuntimeError as exc:
handle(exc)
exc = NoneOr avoid storing exceptions longer than needed.
The retention chain looks like this:
exception
traceback
frame
locals
large objectUnderstanding frames helps explain this behavior.
28.27 Frame Objects Are Introspective, Not Free
Frame introspection is powerful, but it has cost.
Operations such as these can force frame object creation or synchronization:
inspect.currentframe()
sys._getframe()
locals()
traceback inspection
debugger hooks
coverage tracingThis may affect:
performance
memory lifetime
local variable synchronization
optimizer freedom
debugging visibilityFor normal application code, avoid frame introspection in hot paths. For debuggers, profilers, and runtime tools, frame introspection is essential.
28.28 Simplified Frame Execution
A simplified function execution model:
PyObject *
run_function(PyFunctionObject *func, PyObject **args, int nargs)
{
Frame frame;
frame.code = func->code;
frame.globals = func->globals;
frame.builtins = get_builtins(func->globals);
frame.localsplus[0] = args[0];
frame.localsplus[1] = args[1];
frame.stack_pointer = frame.stack;
frame.instruction_pointer = frame.code->first_instruction;
return eval_frame(&frame);
}This omits many real details:
keyword arguments
defaults
closures
cell variables
free variables
generators
coroutines
exceptions
tracing
profiling
reference ownership
specialization
thread state
recursion checksBut the shape is accurate: a function call prepares a frame, then the evaluation loop runs it.
28.29 Simplified Frame Layout
A teaching layout:
typedef struct {
CodeObject *code;
DictObject *globals;
DictObject *builtins;
Object **localsplus;
Object **stack_pointer;
Instruction *instruction_pointer;
ExceptionState exception_state;
struct Frame *previous;
} Frame;The important idea is that local slots and stack values are often stored near each other for efficient execution.
Conceptually:
frame memory
fixed metadata
locals and cells
value stackThe compiler computes how many local slots and stack slots are needed.
28.30 The Localsplus Area
CPython uses a combined area for locals and stack-like data in its execution frames.
A conceptual layout:
localsplus
fast locals
cell variables
free variables
value stackFor:
def f(a, b):
c = a + b
return cThe layout may be understood as:
localsplus
[0] a
[1] b
[2] c
[3...] value stack areaFor closures, cells and free variables also occupy positions known to the code object.
This compact layout reduces allocation overhead and improves locality.
28.31 Frame State Transitions
A frame can move through several states.
created
↓
executing
↓
returnedGenerators and coroutines add more states:
created
↓
suspended
↓
executing
↓
suspended
↓
completedAn exception path:
executing
↓
exception raised
↓
handler found
↓
executing handlerOr:
executing
↓
exception raised
↓
no handler
↓
unwoundFrame state determines what operations are legal. A completed generator cannot resume. A running generator cannot be reentered.
28.32 Reentrant Execution
Python execution can be reentrant.
A frame may execute an instruction that calls user code, which creates another frame before the first instruction finishes.
Example:
class X:
def __add__(self, other):
return 42
def f(a, b):
return a + b
f(X(), X())The BINARY_OP instruction in f calls X.__add__, which executes another Python frame.
Conceptually:
frame f
BINARY_OP
calls __add__
frame X.__add__
return 42
continue frame fThis is why CPython’s evaluation model must preserve frame state across C helper calls that may reenter Python.
28.33 Frames and the C Stack
Python frames and the C stack are related but distinct.
A Python function call creates Python execution state. Depending on CPython version and call path, the C stack may also grow.
The interpreter has worked over time to reduce unnecessary C recursion in Python calls, but native calls, extension calls, and some runtime paths still involve the C stack.
The important distinction:
Python frame
Python-level execution state
C stack frame
native machine call stateA Python traceback shows Python frames, not every C stack frame inside the interpreter.
28.34 Frame Clearing
Frames can be cleared to release references.
A frame references many objects:
locals
globals
builtins
stack values
trace function
exception state
previous frameWhen a frame is no longer needed, CPython must release owned references so objects can be collected.
For generators and coroutines, clearing is more delicate because the frame may be suspended. Closing a generator must release its frame state safely.
Example:
def gen():
data = bytearray(100_000_000)
yield 1
g = gen()
next(g)
del gDeleting the generator can release the suspended frame and therefore release data, assuming no other references exist.
28.35 Frame Inspection Example
This program prints a simple call stack:
import sys
def print_stack():
frame = sys._getframe()
while frame is not None:
print(frame.f_code.co_name, frame.f_lineno)
frame = frame.f_back
def c():
print_stack()
def b():
c()
def a():
b()
a()Conceptually, the chain is:
print_stack
c
b
a
<module>The exact line numbers depend on the file.
This example shows how frames form a linked runtime stack visible to Python code.
28.36 Frame Design Tradeoffs
Frames sit between performance and introspection.
CPython wants frames to be fast because every Python call uses them. But Python also exposes frames to user code and tools.
This creates tension:
| Goal | Pressure |
|---|---|
| Fast calls | Keep frames compact and internal |
| Debugging | Expose rich frame objects |
| Profiling | Preserve call and line metadata |
| Tracebacks | Keep enough state after failure |
| Generators | Support suspension |
| Coroutines | Support async suspension |
| Memory efficiency | Avoid retaining unnecessary references |
| Compatibility | Preserve Python-level frame APIs |
Much CPython frame work is about balancing these constraints.
28.37 Common Misunderstandings
| Misunderstanding | Correct model |
|---|---|
| A frame is the same as a code object | A frame executes a code object |
| Every function object has one frame | Every active call has separate frame state |
| Local variables always live in a dict | Function locals usually use fast slots |
locals() is always the real storage | In functions, it may be a view or synchronized mapping |
| Tracebacks are just strings | Tracebacks reference frames and code positions |
| Generators restart each time | Generators resume a suspended frame |
| Closures keep whole outer frames alive | Normally they keep needed cells alive |
| Frame introspection is free | It can affect performance and memory lifetime |
28.38 Practical Reading Strategy
To study frames in CPython, start from Python-level behavior.
Use:
import dis
import inspect
import sysStudy this function:
def outer(x):
y = x + 1
def inner(z):
return x + y + z
return innerInspect:
fn = outer(10)
print(outer.__code__.co_varnames)
print(outer.__code__.co_cellvars)
print(fn.__code__.co_freevars)
print(fn.__closure__)
dis.dis(outer)
dis.dis(fn)Then map the output to these concepts:
fast locals
cell variables
free variables
code objects
function objects
frames
stack effects
closure cellsThis gives a concrete route from Python syntax to frame internals.
28.39 Chapter Summary
A frame is CPython’s execution record for a running block of Python code. It binds a code object to live runtime state: locals, globals, builtins, stack values, instruction position, exception state, and caller context.
Frames explain function calls, module execution, class bodies, tracebacks, debugging, profiling, recursion, generators, coroutines, closures, and memory retention.
The main model is:
code object
immutable instructions and metadata
frame
mutable execution state for one execution
evaluation loop
runs the frame until return, exception, yield, or suspensionUnderstanding frames makes the evaluation loop concrete. The interpreter is not executing abstract source code. It is advancing frame state through bytecode instructions.