Frame Objects¶
Call Stack¶
1. Function Calls¶
Each call creates frame:
def outer():
return inner()
def inner():
return deepest()
def deepest():
import inspect
return inspect.stack()
# Three frames stacked
2. Frame Structure¶
import inspect
def example():
x = 10
frame = inspect.currentframe()
print(frame.f_locals) # Local vars
print(frame.f_globals) # Global vars
print(frame.f_code) # Code object
Frame Contents¶
1. Local Namespace¶
def function():
a = 1
b = 2
frame = inspect.currentframe()
print(frame.f_locals)
# {'a': 1, 'b': 2, 'frame': ...}
2. Global Namespace¶
GLOBAL_VAR = 100
def function():
frame = inspect.currentframe()
print('GLOBAL_VAR' in frame.f_globals)
# True
Stack Inspection¶
1. Current Frame¶
import inspect
def show_frame():
frame = inspect.currentframe()
print(f"Function: {frame.f_code.co_name}")
print(f"Line: {frame.f_lineno}")
show_frame()
2. Call Stack¶
def outer():
middle()
def middle():
inner()
def inner():
import inspect
for frame_info in inspect.stack():
print(frame_info.function)
# inner, middle, outer
Frame Lifetime¶
1. Creation¶
def function():
# Frame created at call
x = 10
# Frame active
return x
# Frame destroyed after return
2. Nested Calls¶
def a():
return b() # a's frame stays
def b():
return c() # b's frame stays
def c():
return 42 # c returns, frames unwind
Practical Uses¶
1. Debugging¶
def debug_info():
frame = inspect.currentframe()
caller = frame.f_back
print(f"Called from: {caller.f_code.co_name}")
print(f"At line: {caller.f_lineno}")
2. Introspection¶
def get_caller_locals():
frame = inspect.currentframe()
caller_frame = frame.f_back
return caller_frame.f_locals
def example():
x = 10
y = 20
caller_vars = get_caller_locals()
print(caller_vars)
Frame Attributes¶
1. Key Attributes¶
import inspect
def show_attributes():
f = inspect.currentframe()
print(f.f_locals) # Local vars
print(f.f_globals) # Global vars
print(f.f_code) # Code object
print(f.f_lineno) # Current line
print(f.f_back) # Caller frame
Summary¶
1. Frame Object¶
- Created per function call
- Contains local namespace
- Links to globals
- Stack forms call chain
2. Uses¶
- Debugging
- Introspection
- Stack traces
- Context tracking
Runnable Example: frame_introspection.py¶
"""
Frame Object Introspection: Generators, f_lasti, and Zombie Frames
Deep exploration of CPython frame objects using generators as
an introspection tool. Generators freeze execution mid-function,
letting us inspect the frame's internal state at each step.
Topics covered:
1. Generator-based frame stepping (gi_frame, f_lasti)
2. Frame back chain (f_back linked list)
3. Frame locals and code object relationship
4. Zombie frame reuse (memory optimization)
5. Block stack observation via try/except in generators
Based on CPython-Internals Interpreter/frame/frame.md examples.
"""
import dis
import inspect
import sys
# =============================================================================
# 1. Generator Frame Stepping with f_lasti
# =============================================================================
def demo_generator_frame_stepping():
"""Use a generator to observe frame state at each yield point.
Every generator object has a gi_frame attribute pointing to
its execution frame. f_lasti tracks the bytecode offset of
the last executed instruction.
"""
print("=== Generator Frame Stepping ===\n")
def gen(a, b=1, c=2):
yield a
c = str(b + c)
yield c
new_g = range(3)
yield from new_g
# Show bytecode so we can correlate f_lasti values
print("--- Bytecode for gen() ---")
dis.dis(gen)
print()
gg = gen("param_a")
# Before first next(): frame exists but hasn't executed
print(f"Before first next():")
print(f" gi_frame: {gg.gi_frame}")
print(f" f_lasti: {gg.gi_frame.f_lasti}")
print(f" f_locals: {gg.gi_frame.f_locals}")
print()
# Step through and observe f_lasti advancing
step = 0
for val in gg:
step += 1
frame = gg.gi_frame
if frame is not None:
print(f"Step {step}: yielded {val!r}")
print(f" f_lasti: {frame.f_lasti}")
print(f" f_locals: {frame.f_locals}")
print(f" f_lineno: {frame.f_lineno}")
else:
print(f"Step {step}: yielded {val!r}")
print(f" gi_frame is None (generator exhausted)")
print()
# After exhaustion
print(f"After StopIteration:")
print(f" gi_frame: {gg.gi_frame}")
print()
# =============================================================================
# 2. Frame Back Chain (f_back Linked List)
# =============================================================================
def demo_frame_back_chain():
"""Each frame's f_back points to the caller's frame,
forming a singly linked list (the call stack).
"""
print("=== Frame Back Chain ===\n")
def show_stack(depth):
"""Recursively descend, then print the frame chain."""
if depth > 0:
show_stack(depth - 1)
else:
# Walk the f_back chain from current frame
frame = inspect.currentframe()
chain = []
while frame is not None:
chain.append(
f"{frame.f_code.co_name}() "
f"[line {frame.f_lineno}]"
)
frame = frame.f_back
print("Call stack (current -> outermost):")
for i, entry in enumerate(chain):
indent = " " * i
print(f" {indent}{entry}")
print()
show_stack(3)
# =============================================================================
# 3. Frame Locals vs Code Object
# =============================================================================
def demo_frame_code_relationship():
"""The frame's f_code points to the code object being executed.
We can inspect code object metadata through the frame.
"""
print("=== Frame and Code Object Relationship ===\n")
def example_func(x, y, *args, key=None, **kwargs):
"""A function with various parameter types."""
local_var = x + y
frame = inspect.currentframe()
code = frame.f_code
print(f"Function: {code.co_name}")
print(f" co_argcount: {code.co_argcount}")
print(f" co_varnames: {code.co_varnames}")
print(f" co_nlocals: {code.co_nlocals}")
print(f" co_stacksize: {code.co_stacksize}")
print(f" co_firstlineno: {code.co_firstlineno}")
print()
print(f" f_locals keys: {list(frame.f_locals.keys())}")
print(f" f_lineno: {frame.f_lineno}")
print()
example_func(10, 20, 30, 40, key="test", extra=99)
# =============================================================================
# 4. Zombie Frame Reuse
# =============================================================================
def demo_zombie_frame_reuse():
"""CPython reuses frame objects for the same code object.
The first frame created for a code object becomes a 'zombie'
frame after execution. Next call to the same function reuses
that frame, saving malloc overhead.
We can observe this with generators: after a generator is
exhausted, the next generator from the same function often
gets a frame at the same memory address.
"""
print("=== Zombie Frame Reuse ===\n")
def simple_gen():
yield 1
# First generator
g1 = simple_gen()
frame1_id = id(g1.gi_frame)
print(f"g1 frame id: {frame1_id}")
# Exhaust it
list(g1)
print(f"g1 exhausted, gi_frame: {g1.gi_frame}")
# Second generator from same function
g2 = simple_gen()
frame2_id = id(g2.gi_frame)
print(f"g2 frame id: {frame2_id}")
if frame1_id == frame2_id:
print(" -> Same frame object reused (zombie frame)!")
else:
print(" -> Different frame object (may vary by Python version)")
# Exhaust g2 and try a third
list(g2)
g3 = simple_gen()
frame3_id = id(g3.gi_frame)
print(f"g3 frame id: {frame3_id}")
if frame3_id == frame1_id:
print(" -> Same zombie frame reused again!")
print()
# =============================================================================
# 5. Block Stack via Generator Try/Except
# =============================================================================
def demo_block_stack():
"""Generators with try/except blocks demonstrate how the
frame manages exception handling context.
Each try/except/finally creates entries on the frame's
internal block stack (f_iblock in CPython).
"""
print("=== Block Stack via Try/Except in Generator ===\n")
def error_gen():
"""Generator that steps through nested exception handlers."""
try:
yield "in try block"
1 / 0
except ZeroDivisionError:
yield "in except ZeroDivisionError"
try:
yield "in nested try"
import nonexistent_module_xyz # noqa: F401
except ModuleNotFoundError:
yield "in except ModuleNotFoundError"
finally:
yield "in finally"
gg = error_gen()
step = 0
for val in gg:
step += 1
frame = gg.gi_frame
info = ""
if frame is not None:
info = f" f_lasti={frame.f_lasti}, f_lineno={frame.f_lineno}"
print(f" Step {step}: {val!r}{info}")
print(f"\n After exhaustion: gi_frame = {gg.gi_frame}")
print()
# =============================================================================
# 6. sys._current_frames() — All Thread Frames
# =============================================================================
def demo_current_frames():
"""sys._current_frames() returns a dict mapping thread IDs
to their current frame objects. Useful for debugging hangs.
"""
print("=== sys._current_frames() ===\n")
frames = sys._current_frames()
print(f"Active threads: {len(frames)}")
for tid, frame in frames.items():
chain = []
f = frame
while f is not None:
chain.append(f.f_code.co_name)
f = f.f_back
print(f" Thread {tid}: {' -> '.join(chain)}")
print()
# =============================================================================
# Main
# =============================================================================
if __name__ == '__main__':
demo_generator_frame_stepping()
demo_frame_back_chain()
demo_frame_code_relationship()
demo_zombie_frame_reuse()
demo_block_stack()
demo_current_frames()