CPython Bytecode: .pyc Files, dis Disassembly, and the Eval Frame Loop
- Last updated
- Prerequisites:
- Python Syntax and AST
- Stack-based Virtual Machines
- python
- cpython
- bytecode
- dis-module
- pyc
- interpreter
- eval-frame
Read at your depth
The practical view
When Python imports a module, it compiles the source text into **Bytecode** and saves it in the `__pycache__/*.pyc` directory. You can inspect compiled bytecode instructions using the standard library `dis` module: `import dis; dis.dis(my_func)`. Common opcodes include `LOAD_FAST` (load local variable), `LOAD_GLOBAL`, `STORE_FAST`, `CALL`, and `RETURN_VALUE`. CPython executes bytecode using an internal stack-based virtual machine.
The same idea in other frameworks
CPython bytecode disassembler (`dis.dis()`) on stack-based VM.Java Virtual Machine (JVM) bytecode inspected via `javap -c`.V8 Ignition bytecode inspected via `node --print-bytecode`.Lua virtual machine register-based bytecode.Legacy vs modern
Python Source Code vs Compiled Bytecode Disassembly
Simple arithmetic compiles into stack-based bytecode instructions; `dis` reveals opcode specialization and local variable fastpaths.
def add(a, b): return a + bLOAD_FAST 0 (a) -> LOAD_FAST 1 (b) -> BINARY_OP 0 (+) -> RETURN_VALUEInterview gotchas
Context
Interviewers test your knowledge of CPython's stack frame optimization.
The mechanical answer
`LOAD_FAST` reads directly from a fixed-size C array pointer (`co_fastlocal`) inside the `PyFrameObject` using an integer index `O(1)` with zero hashing. `LOAD_GLOBAL` must perform a hash lookup on the module's `__dict__` dictionary, and if not found, perform a second hash lookup on the `builtins.__dict__` dictionary `O(2 * dict_lookup)`. This is why caching globals as local variables in hot loops is a classic optimization.
Trap
In Python 3.11+, `LOAD_GLOBAL` uses inline caching to speed up repeated lookups, but `LOAD_FAST` is still strictly faster.
Context
Checks knowledge of modern Faster CPython JIT and interpreter optimizations.
The mechanical answer
PEP 659 introduces **inline caching and opcode specialization**. When a generic instruction (like `BINARY_OP` or `LOAD_ATTR`) executes repeatedly with the same operand types (e.g. adding two integers), CPython replaces the generic opcode in memory with a specialized opcode (`BINARY_OP_ADD_INT`) that executes optimized C arithmetic directly, skipping generic `PyObject` type checking.
Trap
If the types at that call site change later (de-optimization), CPython replaces the specialized opcode back with the generic adaptive opcode.