How Python Executes Your Code: Bytecode and the PVM Explained
Understand what happens between your Python source code and the machine: bytecode compilation, the Python Virtual Machine, and why it matters for performance and debugging.
What Happens When You Hit "Run" in Python
You write print("Hello, world!"), hit Enter, and text appears on your screen. But what actually happened inside your computer between that keystroke and the output?
Most developers use Python for years without understanding the invisible layer between their code and the machine. That layer is bytecode — and it's simpler than you think.
The Two-Step Dance
Python doesn't go straight from your source code to machine instructions. Instead, it does something clever:
- Compiles your code into an intermediate representation
- Interprets that representation on a virtual machine
This is the same approach Java uses, but with a crucial difference: Python does both steps automatically, without you ever running a separate compiler.
Where Bytecode Lives
When you import a module, Python checks for a .pyc file in the __pycache__ directory. This file contains bytecode that's already been compiled. If the source file is newer than the .pyc file, Python recompiles. If not, it skips straight to execution.
Try this yourself with Pythonskillset's example script:
# demo.py
def greet(name):
return f"Hello, {name}!"
print(greet("Python"))
Import it and Python will create __pycache__/demo.cpython-39.pyc. That .pyc file? Pure bytecode.
Reading the Invisible
You can see exactly what Python compiles your code into:
import dis
def greet(name):
return f"Hello, {name}!"
dis.dis(greet)
This outputs something like:
2 0 LOAD_CONST 1 ('Hello, ')
2 LOAD_FAST 0 (name)
4 FORMAT_VALUE 0
6 BUILD_STRING 2
8 RETURN_VALUE
Each line is a bytecode instruction. Python has about 100 of these total. Your entire program breaks down into combinations of them.
The Python Virtual Machine
Here's where the magic happens. The Python Virtual Machine (PVM) is a loop that:
- Reads the current instruction
- Figures out what to do
- Executes it
- Moves to the next instruction
This loop runs about 100,000 times per second in a typical Python program. The PVM doesn't understand for loops or if statements — it only understands these simple instructions.
The Stack Machine
Python's PVM is a stack-based virtual machine. Imagine a plate stacker at a cafeteria:
- You put values on top (PUSH)
- You take values from top (POP)
- Operations use values from the top
When Python evaluates 2 + 3, it:
- PUSH 2 onto the stack
- PUSH 3 onto the stack
- BINARY_ADD pops both, adds them, pushes result back
For variables, Python uses LOAD_FAST (local variables) or LOAD_GLOBAL (global variables). These instructions copy values from a lookup table onto the stack.
Why This Matters for Performance
Now you understand why Python can be slower than C:
- C code compiles directly to machine instructions that the CPU runs natively
- Python bytecode runs on an interpreter that translates each instruction
The PVM itself is written in C, so there's always a layer of abstraction. Each bytecode instruction might correspond to dozens of actual CPU instructions.
When You Should Care
Understanding bytecode helps you:
- Debug mysterious performance — If you see
LOAD_GLOBALinside a tight loop, that's a red flag - Understand scoping rules — Why
LEGBrule exists (Local, Enclosing, Global, Builtins) - Optimize intelligently — Knowing that local variable access is faster than global access
Example from Pythonskillset's benchmarking:
import timeit
def slow():
import math
return math.sqrt(16)
def fast():
from math import sqrt
return sqrt(16)
# fast() runs about 30% faster
The bytecode shows why: slow() uses LOAD_GLOBAL for math and then LOAD_ATTR for sqrt. fast() just uses LOAD_FAST.
The Full Picture
Your Python journey from source to execution:
- Lexing — Breaking source into tokens
- Parsing — Building an Abstract Syntax Tree
- Compilation — Converting AST to bytecode
- Execution — PVM runs the bytecode loop
You rarely need to think about steps 1-3. But when your code runs slower than expected, or when you hit a weird scoping bug, knowing the bytecode layer gives you superpowers.
Next time you write a Python script, remember: between your readable code and the silicon, there's a beautifully simple stack machine doing exactly what you told it to — one bytecode instruction at a time.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.