How it works
The explanation, with examples you can run.
From text to a code object
When CPython loads a file it tokenises the text, parses it into an abstract syntax tree, and compiles the tree into code objects: one for the module and one for every function and class body inside it. A code object holds the bytecode plus the tables it needs, such as constants and local variable names. You can look at both:
import dis def add(a, b): return a + b dis.dis(add)print(add.__code__.co_varnames) # ('a', 'b')On Python 3.12 the disassembly is:
3 0 RESUME 0 4 2 LOAD_FAST 0 (a) 4 LOAD_FAST 1 (b) 6 BINARY_OP 0 (+) 10 RETURN_VALUERead it as a stack machine: push a, push b, pop both and push their sum, return the top of the stack. The exact instructions change between versions. Python 3.13 fuses the two loads into a single LOAD_FAST_LOAD_FAST and stops printing offsets by default, so never treat bytecode as a stable interface.
The eval loop
Executing a function means creating a frame (its local variables and value stack) and handing it to the evaluation loop, CPython's big switch over opcodes in Python/ceval.c; since 3.12 the instruction bodies are generated from Python/bytecodes.c. Each instruction costs a dispatch plus whatever work it does, and generic work is expensive: BINARY_OP has to look at the types of both operands to decide what + means. This is why a loop of simple arithmetic is slow in Python and why the same loop inside a built-in like sum() or sorted() is fast. The built-in does all the iterations in C.
The interpreter adapts
Python 3.11 added the specialising adaptive interpreter (PEP 659). After an instruction has run a few times, the interpreter rewrites it in place into a version specialised for what it has seen, guarded by a cheap check that falls back if the guess stops being true:
for _ in range(1000): add(1, 2)dis.dis(add, adaptive=True)On 3.12 this shows BINARY_OP_ADD_INT in place of BINARY_OP, and the two loads fused into LOAD_FAST__LOAD_FAST. Your code did not change; the interpreter learned that add is called with ints. This is the main reason 3.11 was noticeably faster than 3.10. Python 3.13 also added an experimental JIT compiler, but it is only present if CPython was built with --enable-experimental-jit, so the standard installers do not use it.
Cached bytecode
Compiling takes time, so when you import a module CPython writes its bytecode to __pycache__/name.cpython-312.pyc (the tag names the interpreter version, PEP 3147). On the next import it checks that file against the source's modification time and size and skips compiling if they match. The script you run directly is always compiled from scratch, and deleting __pycache__ is always safe: it is only a cache.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.