Frames & Call Stack
Every function call creates a frame holding its local state, and the chain of active frames forms the call stack that a traceback prints when an exception escapes.
Every time a Python function is called, the interpreter creates a frame: an object holding that call's local namespace, its position in the executing code, and a reference to the frame that called it. The chain of frames from the currently executing one back to the program's entry point is the call stack. Understanding frames explains what a traceback is actually showing you, why deep recursion eventually fails, and how tools like debuggers and loggers can inspect 'who called this function.'
What Is a Frame
A frame is the runtime record of one function call in progress: it holds that call's local namespace (its local variables), a reference to the code object being executed, the current bytecode instruction position within that code, and a reference to the frame of its caller (so execution can resume there once this call returns). A new frame is created each time a function, method, comprehension (in Python 3, comprehensions get their own scope/frame), generator step, or module body begins executing, and it's discarded (or, for a generator, suspended) when that execution completes.
CPython implementation detail: the exact internal layout of a frame object, and even whether frame objects are eagerly or lazily materialized as Python-visible objects, is CPython-specific and has changed across versions (3.11 significantly reduced the overhead of frame creation). The *concept* of a per-call execution record with locals and a caller link is universal to any correct Python implementation; its precise representation is not.
Local, Global, and Builtin Namespaces
Each frame owns its own local namespace — the mapping from local variable names to the objects they're bound to for that particular call. Name lookup inside a function body follows the LEGB rule: Local (this frame's namespace) first, then Enclosing function scopes (for closures), then the Global (module-level) namespace, then Builtins. locals() and globals() are built-in functions that expose the current frame's local namespace and the current module's global namespace respectively as dictionaries.
x = "global x"
def outer():
y = "enclosing y"
def inner():
z = "local z"
print(locals()) # {'z': 'local z'} — only inner's own frame's locals
print(x) # LEGB: not local/enclosing, found in globals
print(y) # LEGB: found in the enclosing frame via closure
inner()
outer()The Call Stack
Every frame's f_back attribute (or, more portably, what inspect exposes) points to the frame that called it, forming a linked chain from the currently executing frame back to the program's very first frame (the module-level frame of the script or the top-level <module>). This chain is the call stack: it grows by one frame with every function call and shrinks by one when that call returns (or the call raises and the exception propagates up through it).
traceback.print_stack() shows the chain module -> a -> b -> c, each entry a frame currently on the stack.
def a():
b()
def b():
c()
def c():
import traceback
traceback.print_stack() # prints the current call stack, top-down
a()Tracebacks
When an exception propagates out of a function uncaught, Python attaches a traceback object to it: a linked list recording, for every frame the exception passed through, the code location (file, line number, function name) at the moment the exception left that frame. What you see printed when a program crashes — the 'Traceback (most recent call last):' block — is exactly this structure rendered as text, read top-to-bottom from the outermost call to the innermost one where the exception was actually raised.
Each 'File ... line ... in ...' entry corresponds to one frame that was on the call stack when the exception was raised, ordered outermost-first.
def divide(a, b):
return a / b
def compute():
return divide(10, 0)
compute()Traceback (most recent call last):
File "script.py", line 7, in <module>
compute()
File "script.py", line 5, in compute
return divide(10, 0)
File "script.py", line 2, in divide
return a / b
ZeroDivisionError: division by zeroRecursion and Stack Depth
Each recursive call pushes a new frame onto the call stack; deep enough recursion exhausts the space reserved for the stack and raises RecursionError. Python enforces this proactively via sys.getrecursionlimit() (default 1000, adjustable with sys.setrecursionlimit()) rather than only detecting a real memory/stack overflow after the fact — this is a CPython implementation detail meant to fail predictably and safely before a genuine native stack overflow could crash the interpreter itself.
Python does not perform tail-call optimization (unlike some other languages) — a recursive function, even one written in a 'tail-recursive' style, still consumes one stack frame per call. For deep iteration, prefer an explicit loop, or restructure with an accumulator passed through an actual loop, rather than relying on deep recursion.
import sys
def count_down(n):
if n <= 0:
return 0
return 1 + count_down(n - 1)
print(sys.getrecursionlimit()) # 1000 by default
try:
count_down(10_000)
except RecursionError as exc:
print("hit the limit:", exc)Inspecting Frames Programmatically
The standard-library inspect module is the documented, portable way to introspect frames: inspect.currentframe() returns the calling frame, and inspect.stack() returns the full call stack as a list of frame-info records. CPython implementation detail: sys._getframe() offers similar direct access and is commonly used (its leading underscore signals it's an implementation-exposed API, not guaranteed elsewhere) — prefer inspect in code that should be portable across implementations.
log_caller inspects the frame that called it (its own frame's f_back) to report where it was invoked from — a technique used by logging and debugging utilities.
import inspect
def log_caller():
caller_frame = inspect.currentframe().f_back
info = inspect.getframeinfo(caller_frame)
print(f"called from {info.function}() at line {info.lineno}")
def do_work():
log_caller()
do_work()Practical Usage
Frame inspection underlies debuggers (stepping through code means walking and pausing at frames), profilers (sampling which frame is executing at intervals), logging utilities that automatically record the calling function/line, and the traceback module's programmatic APIs (traceback.format_exc(), traceback.extract_stack()) for capturing stack information as data rather than only as printed text. Generators and coroutines also each keep their own suspended frame, which is why a paused generator remembers its local variables between next() calls (see the related Generators topic).
Edge Cases
Holding a reference to a frame object (for example, storing inspect.currentframe() somewhere long-lived) keeps that frame — and everything it references, including all its local variables — alive longer than it otherwise would be, which can create surprising reference cycles or memory retention; inspect's own documentation recommends deleting frame references (del frame) once you're done with them.
sys.setrecursionlimit() raises the *Python-level* recursion ceiling, but the actual native C stack backing the interpreter has its own separate limit; setting the Python limit too high can crash the process with a real stack overflow rather than a catchable RecursionError.
Best Practices
Prefer inspect over sys._getframe() for portability, unless you're specifically writing CPython-only tooling and understand that tradeoff.
Avoid relying on deep recursion for large inputs in production code; rewrite as an explicit loop or use an iterative algorithm when input size isn't bounded and small.
Delete explicit frame references you've captured once you no longer need them, to avoid extending object lifetimes unexpectedly.
When debugging a crash, read a traceback outermost-frame-first (top of the printed block) down to where the exception was actually raised (bottom) — the bottom line names the exception and the actual failure point.
Complete Example
call_chain_depth walks f_back links to count active frames; traceback.print_exc() prints the same chain of frames formatted as the familiar traceback.
import inspect
import traceback
def call_chain_depth() -> int:
"""Counts how many frames are currently on the stack above this one."""
depth = 0
frame = inspect.currentframe().f_back
while frame is not None:
depth += 1
frame = frame.f_back
return depth
def level_three():
print("stack depth here:", call_chain_depth())
raise ValueError("something went wrong at the bottom")
def level_two():
level_three()
def level_one():
level_two()
try:
level_one()
except ValueError:
traceback.print_exc() # prints the full frame chain that led to the errorMental Model
Picture the call stack as a stack of index cards, one added on top each time a function is called: each card lists that call's local variables and a note pointing to the card underneath it (who called it). The currently executing code is always working on the top card. When a function returns, its card is removed. An exception that isn't caught locally gets stamped with 'passed through here' as it's handed down through each card on its way out — that stamped trail is the traceback.
Quick Reference
locals() # current frame's local namespace
globals() # current module's global namespace
inspect.currentframe() # this frame
inspect.currentframe().f_back # the calling frame
inspect.stack() # full call stack as FrameInfo records
sys.getrecursionlimit() # Python-level recursion ceiling (default 1000)
sys.setrecursionlimit(n) # raise/lower it (CPython detail; use with care)
traceback.print_exc() # print the current exception's traceback
traceback.format_exc() # same, as a stringKey Takeaways
A frame is the per-call execution record holding local variables, current code position, and a link to the caller's frame.
The call stack is the chain of active frames from the currently executing call back to the program's entry point.
A traceback is a rendering of the frames an exception passed through, listed outermost-first.
Deep recursion consumes one frame per call and eventually raises RecursionError at Python's configurable recursion limit; Python performs no tail-call optimization.
inspect is the portable way to introspect frames; sys._getframe() is a CPython-specific shortcut.
Holding frame references alive can unexpectedly extend object lifetimes — delete them once done.
def factorial(n): if n <= 1: return 1 return n * factorial(n - 1) factorial(50_000) # RecursionError def factorial(n): result = 1 for i in range(2, n + 1): result *= i return result factorial(50_000) # fine — no growing call stack
Python performs no tail-call optimization, so deep recursion consumes one stack frame per call and can hit RecursionError; an equivalent iterative loop uses a single frame regardless of input size.
saved_frame = inspect.currentframe() #... saved_frame kept around indefinitely, keeping all its locals alive too frame = inspect.currentframe() info = inspect.getframeinfo(frame) del frame # release the reference once the needed info is extracted
A held frame reference keeps every object it can reach (its locals) alive; extract what you need and delete the reference promptly.
What does a frame hold, and when is a new one created?
A frame holds the local namespace for one function call, its current position in the code, and a link to the calling frame; a new one is created each time a function call (or module body, comprehension, or generator step) begins executing.
What is the call stack, concretely?
The chain of frames from the currently executing call back to the program's entry point, linked via each frame's reference to its caller's frame.
What information does each entry in a printed traceback correspond to?
Each 'File ... line ... in ...' entry corresponds to one frame the exception passed through, showing the file, line number, and function active in that frame at the moment the exception left it.
Why does Python raise RecursionError instead of letting deep recursion crash the process?
CPython proactively enforces a configurable recursion limit (default 1000, via sys.getrecursionlimit/setrecursionlimit) so recursion fails with a catchable error before it could exhaust the real, separate native C stack and crash the interpreter.
Does Python perform tail-call optimization? What does that mean for deeply recursive functions?
No — every recursive call, even in tail position, consumes its own stack frame, so deep recursion should be rewritten as an explicit loop for inputs that aren't small and bounded.
What's the difference between using inspect versus sys._getframe() to access frames?
inspect is the documented, portable standard-library API; sys._getframe() is a CPython implementation-specific shortcut (signaled by its leading underscore) not guaranteed to exist in other implementations.
Why might holding a reference to a frame object cause a memory issue?
A retained frame reference keeps everything reachable from its local namespace alive, potentially extending object lifetimes far beyond when the call actually finished, or creating reference cycles.
Write a function current_function_name() that uses inspect to return the name of the function it was called from (its caller), without hardcoding the caller's name.
import inspect def current_function_name() -> str: caller_frame = inspect.currentframe().f_back return caller_frame.f_code.co_name def my_function(): print(current_function_name()) my_function()
Prints 'my_function' — f_back gets the caller's frame, and f_code.co_name reads that frame's code object's function name.
In the order it would appear in the printed traceback (outermost first), which functions/frames does the traceback show?
The module level (calling a()), then a (calling b()), then b (calling c()), then c (where the ZeroDivisionError is actually raised on 1 / 0) — outermost caller first, innermost failing frame last, matching the order frames were pushed onto the call stack.
Write a recursive function sum_to(n) using simple recursion, then rewrite it iteratively, and explain which one is safe for n = 100000.
def sum_to_recursive(n: int) -> int: if n <= 0: return 0 return n + sum_to_recursive(n - 1) def sum_to_iterative(n: int) -> int: total = 0 for i in range(1, n + 1): total += i return total print(sum_to_iterative(100_000)) # safe: one frame total # sum_to_recursive(100_000) would raise RecursionError: exceeds the default limit sum_to_iterative is safe for n=100000 because it uses a single frame with a loop; sum_to_recursive would raise RecursionError because it pushes 100,000 nested frames, far exceeding CPython's default recursion limit of 1000.
def a():
return b()
def b():
return c()
def c():
return 1 / 0
a()