Skip to content

28. Frames

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 code

For this function:

def add(a, b):
    c = a + b
    return c

The 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 executing

Each 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.

ConceptCode objectFrame
MutabilityMostly immutableMutable
LifetimeOften long-livedUsually one call or suspended execution
Contains bytecodeYesReferences code object
Contains localsNoYes
Contains stackNoYes
Contains instruction pointerNoYes
Shared across callsYesNo

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 name

When 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 = start

The 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 x

CPython 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 = 10

creates:

module.__dict__["x"] = 10

Function 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 User

During 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.

CategoryPurpose
Code referenceThe code object being executed
GlobalsGlobal namespace for name lookup
BuiltinsBuiltin namespace fallback
LocalsLocal variables or namespace
Fast localsSlot array for function locals
Value stackTemporary operands for bytecode
Instruction stateCurrent bytecode position
Exception stateActive exception handling metadata
Caller relationLink to previous frame or call context
Tracing stateDebugger, profiler, coverage hooks
Owner stateGenerator, 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 flags

The 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 c

The compiler knows the local names:

a
b
c

It assigns them indexes:

NameSlot
a0
b1
c2

During execution, CPython can access locals by slot:

LOAD_FAST 0
LOAD_FAST 1
STORE_FAST 2
LOAD_FAST 2

This 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: c

This 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 x

You 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 NameError

The frame provides both dictionaries:

frame.globals  = module namespace
frame.builtins = builtins namespace

Conceptually:

def f(xs):
    return len(xs)

runs as:

LOAD_GLOBAL len
LOAD_FAST xs
CALL 1
RETURN_VALUE

LOAD_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) * c

The stack changes roughly like this:

InstructionStack beforeStack 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 object

Bytecode 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 x

Conceptual control flow:

load x
load 0
compare <
jump if false to return_x
load x
unary negative
return
return_x:
load x
return

The frame records which instruction is next. This position matters for:

normal execution
branches
loops
exceptions
tracebacks
line tracing
profiling
debugging

Tracebacks 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
ZeroDivisionError

Each 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:

AttributeMeaning
f_codeCode object
f_localsLocal namespace view
f_globalsGlobal namespace
f_builtinsBuiltins namespace
f_backPrevious frame
f_linenoCurrent source line
f_traceTrace 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 alive

Example:

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 frame

This 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 references

For 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 x

Calling 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 state

The 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 data

At await, the coroutine can suspend. Its frame retains:

local variables
current instruction position
pending awaitable
exception state
return path

An event loop later resumes it.

coroutine object
    suspended frame or frame-like state
        locals
        stack
        instruction pointer

Async 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 state

This 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 inner

The 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 cell

After 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.

TermMeaning
Cell variableLocal variable captured by an inner function
Free variableVariable used here but defined in an outer scope

Example:

def outer():
    x = 1

    def inner():
        return x

    return inner

For 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 0

The 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 handler

If 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 exception

This 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 monitors

But 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 = 1

All 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 = exc

The 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 = None

Or avoid storing exceptions longer than needed.

The retention chain looks like this:

exception
    traceback
        frame
            locals
                large object

Understanding 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 tracing

This may affect:

performance
memory lifetime
local variable synchronization
optimizer freedom
debugging visibility

For 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 checks

But 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 stack

The 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 stack

For:

def f(a, b):
    c = a + b
    return c

The layout may be understood as:

localsplus
    [0] a
    [1] b
    [2] c
    [3...] value stack area

For 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
returned

Generators and coroutines add more states:

created
suspended
executing
suspended
completed

An exception path:

executing
exception raised
handler found
executing handler

Or:

executing
exception raised
no handler
unwound

Frame 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 f

This 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 state

A 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 frame

When 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 g

Deleting 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:

GoalPressure
Fast callsKeep frames compact and internal
DebuggingExpose rich frame objects
ProfilingPreserve call and line metadata
TracebacksKeep enough state after failure
GeneratorsSupport suspension
CoroutinesSupport async suspension
Memory efficiencyAvoid retaining unnecessary references
CompatibilityPreserve Python-level frame APIs

Much CPython frame work is about balancing these constraints.

28.37 Common Misunderstandings

MisunderstandingCorrect model
A frame is the same as a code objectA frame executes a code object
Every function object has one frameEvery active call has separate frame state
Local variables always live in a dictFunction locals usually use fast slots
locals() is always the real storageIn functions, it may be a view or synchronized mapping
Tracebacks are just stringsTracebacks reference frames and code positions
Generators restart each timeGenerators resume a suspended frame
Closures keep whole outer frames aliveNormally they keep needed cells alive
Frame introspection is freeIt 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 sys

Study this function:

def outer(x):
    y = x + 1

    def inner(z):
        return x + y + z

    return inner

Inspect:

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 cells

This 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 suspension

Understanding frames makes the evaluation loop concrete. The interpreter is not executing abstract source code. It is advancing frame state through bytecode instructions.