Bytecode & How CPython Runs Your Code
AdvancedCPython compiles source to bytecode (inspect it with dis), caches it in __pycache__, and executes it in the interpreter loop — since 3.11, an adaptive specializing interpreter rewrites hot bytecode for big speedups.
Overview
Python is not "purely interpreted": your .py file is first compiled to bytecode — instructions for CPython's stack-based virtual machine — then that bytecode is executed by the interpreter loop. The dis module shows the exact instructions, which is the ground truth for questions like "is x += 1 atomic" or "why are local variables faster than globals" (LOAD_FAST is an array index; LOAD_GLOBAL is dict lookups). Compiled bytecode is cached as .pyc files in __pycache__, which is why the second import of a module is faster. Since Python 3.11 the interpreter is adaptive: hot instructions specialize themselves to observed types, a major reason 3.11+ is 25-60% faster — upgrading Python versions is itself a performance optimization.
dis — Reading the Bytecode
dis.dis shows what a function compiles to. Locals use LOAD_FAST (indexed array access); globals use LOAD_GLOBAL (namespace lookup) — the mechanical reason hot loops run faster on local variables.
import dis
def total_marks(marks):
total = 0
for m in marks:
total += m
return total
dis.dis(total_marks)
# LOAD_CONST 1 (0) ← push 0
# STORE_FAST 1 (total) ← total is slot 1: array access, fast
# LOAD_FAST 0 (marks)
# GET_ITER
# FOR_ITER ...
# LOAD_FAST 1 (total) ← load, add, store: three separate steps
# LOAD_FAST 2 (m)
# BINARY_OP 13 (+=)
# STORE_FAST 1 (total) ← a thread could be switched mid-sequence:
# ... this is WHY += is not atomic
import math
def use_global(): return math.pi # LOAD_GLOBAL — dict lookups
def use_local():
pi = math.pi # bind once...
return pi # LOAD_FAST — array index.pyc Caching and the 3.11+ Adaptive Interpreter
Bytecode compilation happens once per source change; __pycache__ stores the result keyed by interpreter version. From 3.11, hot code paths specialize: generic instructions rewrite themselves for the types actually seen, and 3.12/3.13 build on this with a JIT foundation.
# __pycache__/utils.cpython-312.pyc ← compiled bytecode cache
# - created on first import, reused while source is unchanged
# - source mtime/hash checked → edit .py and it recompiles automatically
# - version-tagged: 3.12 and 3.13 caches coexist side by side
# - this is compile-to-bytecode caching, NOT machine-code compilation
# The specializing adaptive interpreter (PEP 659, Python 3.11+):
def add(a, b):
return a + b
for _ in range(100):
add(2, 3)
# After warm-up, generic BINARY_OP specializes to BINARY_OP_ADD_INT —
# a fast path that skips type dispatch while both args stay ints.
# Specializations deoptimize automatically if types change: add("a", "b") still works.
# Practical takeaways:
# - Upgrading 3.10 → 3.12 is often a free 25-60% speedup
# - Type-stable hot loops (same types every iteration) specialize best
# - python -X importtime app.py → per-module import cost breakdown
# - Python 3.13+: experimental copy-and-patch JIT builds on these tiersKey Points to Remember
- 1Source compiles to bytecode for a stack VM; dis.dis shows the instructions
- 2LOAD_FAST (locals, array index) beats LOAD_GLOBAL (dict lookup) — locals are faster
- 3__pycache__/.pyc caches bytecode per interpreter version; auto-invalidates on edit
- 4PEP 659 adaptive specialization makes 3.11+ dramatically faster — upgrades are free perf
Interview Questions
Sign in to ask AriaIs Python compiled or interpreted? Explain what actually happens to a .py file.
Use dis to prove that x += 1 is not thread-safe.
Why did many services get 30-50% faster just by moving to Python 3.11+?
Ask Aria about Bytecode & How CPython Runs Your Code
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.