What is Python Bytecode?
Python bytecode is an intermediate representation of Python source code. When you run a Python program, it's first compiled to bytecode, which is then executed by the Python Virtual Machine (PVM).
Python Bytecode Visualization
Python Source
def add(a, b):
return a + b
result = add(3, 5)Bytecode Instructions
How Python Bytecode Works
- • Python compiles source code to bytecode before execution
- • Bytecode is platform-independent and cached in .pyc files
- • The Python Virtual Machine (PVM) executes bytecode instructions
- • Each instruction manipulates the value stack and local/global namespaces
The Compilation Process
1. Source Code to Tokens
Python first tokenizes your source code:
# Source code def add(a, b): return a + b # Tokens NAME 'def' NAME 'add' OP '(' NAME 'a' OP ',' NAME 'b' OP ')' OP ':' # ...
2. Tokens to AST
The tokens are parsed into an Abstract Syntax Tree:
import ast import inspect def add(a, b): return a + b # Get the AST tree = ast.parse(inspect.getsource(add)) print(ast.dump(tree))
3. AST to Bytecode
The AST is compiled to bytecode instructions:
import dis def add(a, b): return a + b dis.dis(add) # Output: # 2 0 LOAD_FAST 0 (a) # 2 LOAD_FAST 1 (b) # 4 BINARY_ADD # 6 RETURN_VALUE
Understanding Bytecode Instructions
Common Instructions
| Instruction | Description | Stack Effect |
|---|---|---|
LOAD_FAST | Load local variable | Push value |
LOAD_GLOBAL | Load global variable | Push value |
STORE_FAST | Store to local variable | Pop value |
BINARY_ADD | Add two values | Pop 2, push 1 |
CALL_FUNCTION | Call a function | Pop args+func, push result |
RETURN_VALUE | Return from function | Pop value |
POP_TOP | Remove top of stack | Pop 1 |
JUMP_IF_FALSE | Conditional jump | Check top |
The Value Stack
Python's VM is stack-based. Operations manipulate a value stack:
# Expression: a + b * c # Bytecode execution: LOAD_FAST 0 (a) # Stack: [a] LOAD_FAST 1 (b) # Stack: [a, b] LOAD_FAST 2 (c) # Stack: [a, b, c] BINARY_MUL # Stack: [a, b*c] BINARY_ADD # Stack: [a+b*c]
Bytecode Caching
.pyc Files
Python caches compiled bytecode in __pycache__ directories:
mymodule.py __pycache__/ mymodule.cpython-311.pyc # Python 3.11 bytecode
Cache Validation
Python checks if the source has changed:
- Compare modification timestamps
- Compare source hash (Python 3.7+)
- Recompile if needed
Inspecting Bytecode
Using the dis Module
import dis # Disassemble a function def factorial(n): if n <= 1: return 1 return n * factorial(n - 1) dis.dis(factorial)
Bytecode Objects
# Access bytecode directly code = factorial.__code__ print(f"Argument count: {code.co_argcount}") print(f"Local variables: {code.co_nlocals}") print(f"Stack size: {code.co_stacksize}") print(f"Constants: {code.co_consts}") print(f"Variable names: {code.co_varnames}") print(f"Bytecode: {code.co_code.hex()}")
Control Flow in Bytecode
Conditional Execution
def check_positive(x): if x > 0: return "positive" return "non-positive" # Bytecode uses jumps: # LOAD_FAST 0 (x) # LOAD_CONST 1 (0) # COMPARE_OP 4 (>) # POP_JUMP_IF_FALSE to 8 # LOAD_CONST 2 ('positive') # RETURN_VALUE # LOAD_CONST 3 ('non-positive') # RETURN_VALUE
Loops
def sum_range(n): total = 0 for i in range(n): total += i return total # Loop bytecode uses: # GET_ITER # FOR_ITER # JUMP_ABSOLUTE (back to loop start)
Python 3.11+ Improvements
Adaptive Bytecode
Python 3.11 specializes bytecode based on runtime behavior:
def add_numbers(a, b): return a + b # First calls: BINARY_ADD (generic) # After ~8 int additions: BINARY_ADD_INT (specialized)
Inline Caching
Frequently accessed attributes are cached inline:
# Before: LOAD_ATTR requires dictionary lookup # After: LOAD_ATTR_SLOT uses cached offset
Performance Implications
What's Fast
- Local variable access (LOAD_FAST)
- Built-in operations
- Specialized bytecode (3.11+)
What's Slow
- Global variable access (LOAD_GLOBAL)
- Attribute lookup (LOAD_ATTR)
- Function calls (CALL_FUNCTION)
Practical Example
# Original code def process_list(items): result = [] for item in items: if item > 0: result.append(item * 2) return result # More efficient (fewer bytecode instructions) def process_list_optimized(items): return [item * 2 for item in items if item > 0]
Related concepts
Explore how C++ code is parsed into an Abstract Syntax Tree (AST). Learn lexical analysis, tokenization, and syntax parsing for systems programming.
Understand the complete C++ compilation pipeline from source code to object files. Learn preprocessing, parsing, code generation, and optimization stages.
Learn the CPython Global Interpreter Lock (GIL) from first principles: why it exists, how threads take turns, why I/O still works well, and when to use multiprocessing, asyncio, or native extensions.
A practical mental model for CPython memory management: names and references, object headers, PyMalloc arenas, reference counting, reuse paths, and memory profiling.
Learn how CPython implements PyObject, type objects, and the unified object model. Explore reference counting, memory layout, and Python internals.
C++ preprocessor visualized: macros, header guards, conditional compilation, and #include directives explained interactively.
