CPython Runtime, AST, Bytecode & Frame Execution

Understanding how Python source code executes requires stepping into the CPython C Runtime Engine. From text tokenization and Abstract Syntax Tree (AST) parsing to compilation into bytecode (PyCodeObject), execution frame allocation (PyFrameObject), and evaluation inside ceval.c (_PyEval_EvalFrameDefault), this chapter unveils CPython’s execution mechanics.

This chapter details CPython compilation phases, PyCodeObject struct contents, PyFrameObject stack frame layouts, and Python 3.11+ Specializing Adaptive Interpreter (PEP 659) opcode acceleration.


1. The CPython Compilation & Execution Pipeline

Executing python app.py triggers an explicit 4-phase transformation:

CPython Compilation & Execution Pipeline:

[ Source Code (.py) ]
          |
          v (Tokenizer & Parser)
[ Concrete / Abstract Syntax Tree (AST) ]
          |
          v (Compiler)
[ Bytecode (PyCodeObject saved to __pycache__/*.pyc) ]
          |
          v (Interpreter Engine: ceval.c)
[ Execution Frame (PyFrameObject) ] ──> [ CPU Execution Loop ]
  1. Tokenizer & Parser: Scans raw source characters, emitting a stream of tokens that build an Abstract Syntax Tree (AST).
  2. Compiler: Translates AST nodes into a PyCodeObject containing bytecode instruction bytes, variable arrays, and constant tables.
  3. Frame Evaluator (ceval.c): Allocates a PyFrameObject on the C stack and passes it to _PyEval_EvalFrameDefault(), which executes opcodes in a giant switch/computed-goto loop.

2. Code Objects (PyCodeObject) vs. Execution Frames (PyFrameObject)

  • PyCodeObject (Static Read-Only Blueprint): Immutable struct compiled once per function. Contains raw bytecode bytes (co_code), constant tuples (co_consts), variable name tuples (co_varnames, co_names), and stack depth bounds (co_stacksize).
  • PyFrameObject (Dynamic State Instance): Heap/stack allocated struct instantiated every time a function is called. Manages dynamic execution state:
PyFrameObject Memory Layout:

[ PyFrameObject ]
β”œβ”€β”€ f_code: Pointer to PyCodeObject
β”œβ”€β”€ f_globals: Module globals dict
β”œβ”€β”€ f_localsplus: C array of Fast Local Variables (LOAD_FAST)
└── f_valuestack: Pointer to current top of Evaluation Value Stack

3. Python 3.11+ Specializing Adaptive Interpreter (PEP 659)

Python 3.11 introduced a massive performance engine: the Specializing Adaptive Interpreter:

  • Inline Caching: Generic opcodes (like BINARY_SUBSCR or LOAD_ATTR) inspect runtime operand types over repeated executions.
  • Opcode Specialization: If a LOAD_ATTR operation repeatedly accesses an attribute on a specific class, CPython rewrites the bytecode opcode in-place to a specialized variant (LOAD_ATTR_MODULE or LOAD_ATTR_SLOT), bypassing generic dictionary lookups and executing 10x to 100x faster!
Adaptive Opcode State Machine:

[ Generic Opcode (e.g. LOAD_ATTR) ] ──(Executed 8 times)──> [ Warm State ]
                                                                  |
                                                           (Type is consistent)
                                                                  v
[ Optimized Specialized Opcode (LOAD_ATTR_SLOT) ] <────── [ Specialized State ]

4. Production Trade-offs & Bytecode Inspection

  • Disassembling Bytecode (dis): Use Python’s built-in dis module to inspect bytecode instructions (dis.dis(func)) and verify opcode counts when tuning performance-critical loops.
Display Options
Appearance
Text Size
100%