Python
What Happens When You Run a Python Script: The Full Pipeline Explained
From lexing to bytecode to the Python Virtual Machine, this article walks through every step Python takes to execute your script—and explains why it's not magic, just clever engineering.
June 2026 · 6 min read · 1 views · 0 hearts
Advertisement
Have you ever run python my_script.py and wondered what actually happens inside your machine? It's not magic—though it can feel that way. Let's peel back the layers and follow your Python code from the moment you hit Enter to the final output.
The Interpreter Smells Like C
When you install Python, you're really installing the CPython interpreter—a program written in C that knows how to read, translate, and run Python code. But it doesn't just "run" your script line by line in the way you might imagine. The process is surprisingly sophisticated.
Stage 1: Lexing — Breaking Down the Noise
Your Python file is just a stream of characters: print("Hello") is p, r, i, n, t, (, ", etc. The interpreter's first job is tokenizing. It reads these characters and groups them into meaningful chunks called tokens.
- Keywords like
def,if,while - Identifiers like
my_variable - Operators like
+or== - Literals like
42or"Hello"
This step strips away whitespace and comments, leaving a clean list of tokens ready for the next stage.
Stage 2: Parsing — Building the Blueprint
The token stream is meaningless without structure. The parser takes those tokens and builds an Abstract Syntax Tree (AST)—a tree-like representation of your code's logical structure.
For x = 5 + 3, the AST becomes:
Assign(target=x, value=BinOp(left=5, op=+, right=3))
This AST is the backbone that tells Python what your code means, not how to run it yet. You can even inspect this yourself:
import ast
code = "x = 5 + 3"
print(ast.dump(ast.parse(code)))
Stage 3: Compilation — The Surprising Step
Here's where most people get surprised: Python compiles your code. It takes the AST and generates bytecode—a low-level, platform-independent instruction set for the Python Virtual Machine (PVM).
Bytecode isn't machine code. You won't find it in your CPU's instruction set. It's designed specifically for Python's runtime. You can see it with:
import dis
dis.dis("x = 5 + 3")
Which outputs something like:
1 0 LOAD_CONST 0 (5)
2 LOAD_CONST 1 (3)
4 BINARY_ADD
6 STORE_NAME 0 (x)
Each line is a bytecode instruction. LOAD_CONST pushes a constant onto a stack. BINARY_ADD pops two values, adds them, and pushes the result. It's stack-based, simple, and fast enough.
Stage 4: The Python Virtual Machine Takes Over
The compiled bytecode is handed to the PVM—a big loop written in C that reads each instruction and executes it.
LOAD_CONST→ C function pushes value onto Python's internal stackBINARY_ADD→ C function callsPyNumber_Add()which checks object types, calls__add__methodsSTORE_NAME→ C function stores the result in a namespace dictionary
This loop is the heart of Python. It's why Python code can be dynamic: every operation goes through that C-level dispatch, letting you redefine classes, monkey-patch functions, or even change types at runtime.
The Hidden Costs: Why Python Isn't "Fast"
Understanding this pipeline reveals Python's performance bottlenecks:
- Every variable lookup means a dictionary search
- Every integer addition checks object types before doing the math
- Every function call involves stack frame creation and garbage collection
That's why Python is slower than C or Rust—not because of the language itself, but because every bytecode instruction has to do runtime validation that compiled languages handle at compile time.
But Wait: There's a Cache
Python doesn't recompile everything every time. When you import a .pyc file, Python checks timestamps and reuses the cached bytecode. This is stored in __pycache__/ directories. It's why second imports are faster than the first.
What About JIT Compilation?
CPython doesn't have a JIT compiler by default. But PyPy (a Python implementation) does. PyPy compiles hot bytecode loops directly into machine code, sometimes giving 10x speedups—while still running the same Python code.
The Takeaway
Next time you run a Python script, picture this chain:
- Characters → Tokens (lexer)
- Tokens → AST (parser)
- AST → Bytecode (compiler)
- Bytecode → C-level execution (PVM loop)
It's not magic. It's a carefully designed pipeline that trades raw execution speed for incredible flexibility and ease of use. And every time you write x += 1, you're actually triggering a cascade of C functions, stack operations, and runtime checks—all without ever having to think about it.
That's the beauty of Python's design.
Advertisement
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.