CPython Evaluation Loop: Inside _PyEval_EvalFrameDefault
The _PyEval_EvalFrameDefault function is the core
execution engine of CPython, responsible for interpreting compiled
Python bytecode into runtime actions. Located primarily within
Python/ceval.c (and modularized execution tables in modern
Python versions), this function manages the virtual machine's stack,
dispatches opcodes, handles function calls, manages exceptions, and
coordinates system-level interrupts and thread switches.
Frame Initialization and Context Setup
When a Python function is invoked, CPython creates an execution
frame—represented in modern CPython (3.11+) as an
_PyInterpreterFrame. When
_PyEval_EvalFrameDefault receives this frame, it
initializes several register-level C variables for maximum
performance:
- Instruction Pointer (
next_instr): Points to the current bytecode instruction inside the code object (co_code). - Stack Pointer (
stack_pointer): Points to the top of the evaluation stack. CPython is a stack-based virtual machine, meaning values are pushed and popped to perform operations. - Locals and Globals Arrays: Pointers to the
fast-access local variable array (
localsplus), cell/free variables for closures, and dictionaries for globals and builtins.
The Fetch-Decode-Execute Cycle
The core of _PyEval_EvalFrameDefault is an infinite loop
that repeatedly fetches, decodes, and executes bytecode
instructions.
1. Instruction Fetching
Bytecode is stored as an array of 16-bit code units (in Python
3.10+). Each code unit consists of an 8-bit opcode and an 8-bit argument
(oparg). If an argument exceeds 8 bits, an
EXTENDED_ARG prefix instruction is used to shift and
accumulate bits before reaching the true operation.
2. Opcode Dispatching
CPython uses two primary strategies to jump to the code that handles a specific instruction:
- Computed
goto(Direct Threading): On supported compilers (like GCC and Clang), CPython uses an array of jump labels. At the end of every instruction, the loop reads the next opcode and directly jumps to its handler address via an indirectgoto. This avoids the branch prediction penalty of a traditional loop. - Standard
switch: On compilers lacking computedgotosupport, CPython falls back to a massiveswitch(opcode)statement enclosed within afor(;;)loop.
3. Execution and Stack Manipulation
Each instruction block contains raw C code that alters the state of the machine. For instance:
LOAD_FASTreads a pointer directly from the local variable array and pushes it onto the value stack.BINARY_OPpops two operands from the stack, executes the corresponding C-level slot function (e.g.,PyNumber_Add), and pushes the result back onto the stack.STORE_FASTpops the top value off the stack and writes it into the local variable array.
Periodic Checks and Interrupt Handling
Inside the loop, CPython periodically checks whether it must service
external requests. This mechanism is controlled by an evaluation breaker
flag (eval_breaker):
- Signals: If an OS-level signal (like
SIGINT/ Ctrl+C) arrives, the loop halts standard execution to invoke Python signal handlers. - Thread Scheduling and the GIL: When multiple threads run, the Global Interpreter Lock (GIL) must be periodically released. The evaluation loop checks a tick or counter mechanism to yield the GIL to other waiting threads.
- Asynchronous Callbacks: Functions scheduled via
Py_AddPendingCallare executed when the eval breaker triggers.
Exception Handling and Stack Unwinding
If any C API call within an instruction returns NULL
(indicating an error), the evaluation loop transitions to
exception-handling mode:
- Exception Table Lookup: Modern CPython checks the code object's exception table to find an offset matching the current instruction pointer.
- Stack Adjustment: If an enclosing
try...exceptortry...finallyblock is found, the value stack is unwound to the depth expected by the handler, the exception state is pushed, andnext_instrjumps to the handler's bytecode offset. - Frame Unwinding: If no handler exists within the
current frame,
_PyEval_EvalFrameDefaultcleans up the current frame, sets the exception in the thread state (PyThreadState), and returnsNULLto the caller, propagating the exception up the call stack.
Adaptive Specialization (Python 3.11+)
In modern CPython architectures,
_PyEval_EvalFrameDefault incorporates the specializing
adaptive interpreter (PEP 659).
Instructions monitor their execution patterns at runtime. For
example, a generic LOAD_ATTR instruction tracks the types
of objects passing through it. Once a type stabilizes, the interpreter
overwrites the instruction in-place with a specialized opcode (such as
LOAD_ATTR_INSTANCE_VALUE) alongside an inline cache entry.
In subsequent iterations of the loop, the specialized opcode bypasses
generic dictionary lookups, directly fetching attributes via precomputed
memory offsets. If type assumptions are invalidated, the instruction
de-optimizes back to its generic form.
Exiting the Frame
The loop terminates when it encounters instructions like
RETURN_VALUE or when an unhandled exception causes an exit.
The function decrements references to remaining objects on the value
stack, adjusts the interpreter frame hierarchy to point back to the
caller frame, and returns the final PyObject* to the
calling context.