Topic 0.8
How Python Really Runs Your Code
In one line
When you run a file, Python first compiles your source code into bytecode, a compact set of simple instructions, and then a virtual machine carries those instructions out one by one. The standard Python that does this is called CPython; knowing these steps explains Python's error behaviour and its speed.
Think of it like this
A play. The writer's script (your source code) is first turned into a stage manager's cue sheet: short, numbered instructions like "lights up", "actor enters left" (the bytecode). During the show, the stage manager reads the cue sheet and calls each cue in order (the virtual machine). The audience only ever sees the result.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Source code
- The text you write, in a
.pyfile. - Compile
- To translate code from one form to another. Python compiles source code to bytecode.
- Bytecode
- Simple, compact instructions that the Python virtual machine carries out. You don't write it; Python produces it.
- Virtual machine (VM)
- A program that acts like a computer and runs bytecode instructions one at a time.
- Interpreter
- The whole
pythonprogram: it compiles your source to bytecode and runs it on the virtual machine. - CPython
- The standard implementation of Python, written in C. What you download from python.org.
- Implementation
- A particular program that runs the Python language. CPython, PyPy and MicroPython are implementations.
- __pycache__
- A folder where Python saves compiled bytecode of imported files, in
.pycfiles, to start faster next time. - JIT
- Just-in-time compiler: it translates frequently run code into the processor's own instructions while the program runs.
- Machine code
- The instructions a processor understands directly.
Step by step
01From file to output
Follow python hello.py all the way. The interpreter reads the text of the file, compiles it to bytecode, and hands the bytecode to the virtual machine. The virtual machine runs it, asking the operating system to put text on the screen when it meets a print.
Two kinds of error map onto two stages: syntax errors come from the compile stage, exceptions from the run stage.
02See the compile step
Python lets you do the two steps by hand. compile() turns source text into a code object holding bytecode; exec() runs it. type(code).__name__ prints the kind of thing code is, and code.co_names lists the names the code uses.
The code object's co_consts lists the constant values in the code. Look closely: the source says 2 + 3, but the constants contain 5. The compiler worked out 2 + 3 once, during compilation, so the virtual machine never has to add them. (The None is the value every finished piece of code hands back.)
source = "print(2 + 3)"
code = compile(source, "tiny.py", "exec")
print(type(code).__name__)
print(code.co_consts)
print(code.co_names)
exec(code)03Look at bytecode with dis
The standard library's dis module (short for disassemble) shows bytecode in readable form. Below is the bytecode for a one-line function that adds 18% tax to a price. (def makes a function and return gives back its answer; Phase 5 teaches both.)
Read the right-hand part. LOAD_FAST_LOAD_FAST puts price on a work pile (the stack) twice. LOAD_CONST puts 0.18 on it. The first BINARY_OP multiplies the top two values; the second adds the result to the remaining price. RETURN_VALUE hands back the answer. RESUME at the top is bookkeeping that every function starts with. One short line of Python became six instructions. The numbers on the far left are the source line numbers.
import dis
def add_tax(price):
return price + price * 0.18
dis.dis(add_tax)04The __pycache__ folder
When one of your files imports another (import helper loads the file helper.py; Phase 6), Python saves the imported file's bytecode. For helper.py on Python 3.13 the saved file is __pycache__/helper.cpython-313.pyc. The name records the implementation and version, so different Pythons don't mix up each other's files.
The file you run directly (main.py) is not cached, only what it imports. The cache makes starting faster. It does not make the code run faster: the virtual machine executes the same bytecode either way.
05Interpreted or compiled?
Languages such as C, C++, Rust and Go are compiled ahead of time to machine code: a separate build step produces a program the processor runs directly. That is fast to run and slow to change.
Python compiles to bytecode at the moment you run the file and interprets that bytecode. Java sits in between: it compiles to bytecode ahead of time, and its virtual machine has a mature JIT. "Interpreted" and "compiled" are properties of an implementation, not of a language, but in everyday talk Python is interpreted.
06Which Python is this?
A program can ask which implementation is running it. On your computer the answer is almost certainly CPython. On this page it is also CPython, but built for a different kind of "computer": your browser, through a technology called WebAssembly. That build is Pyodide.
Same language, same bytecode, same results. What differs is the surroundings: the browser has no terminal keyboard and does not allow programs to start threads (extra workers inside one program) or other programs.
import sys
import platform
print(platform.python_implementation())
print(sys.platform)07What this means for speed
Each bytecode instruction costs the virtual machine real work, so a Python loop that runs a billion times is slow. Three practical lessons follow, and later phases build on each.
One: let built-in functions and libraries do loops for you; sum(numbers) runs its loop in C. Two: for heavy maths, use NumPy, which does the whole calculation in compiled code (Topic 0.2, Phase 14). Three: most programs spend their time waiting for files, networks and model APIs, where the interpreter's speed hardly matters (Phase 12).
Try it yourself
- 1
Watch the compiler do arithmetic
In the first example, change the source to
"print(10 * 10 + 1)". Predict whatco_constswill contain before running. The compiler folds the whole constant calculation into one number. - 2
Change the bytecode
In the second example, change the function body to
return price * 2. Run it and compare: fewer instructions, and a differentLOAD_line. Then tryreturn price + 1. - 3
Find a __pycache__ folder
On your computer, create
helper.pycontainingGREETING = "Hello"andmain.pycontainingimport helperandprint(helper.GREETING). Runpython main.py, then list the folder. Delete__pycache__and run again: it comes back.terminal$ python main.pyls── expected output ──Hello__pycache__ helper.py main.py
Code & diagrams
The source says 2 + 3 but the compiled constants contain 5: the compiler did the sum in advance.
Expected output
code
(5, None)
('print',)
20 bytes of bytecode
5Six small instructions for one line of Python. Change the function to return price * 2 and run again to see the bytecode change.
Expected output
3 RESUME 0
4 LOAD_FAST_LOAD_FAST 0 (price, price)
LOAD_CONST 1 (0.18)
BINARY_OP 5 (*)
BINARY_OP 0 (+)
RETURN_VALUECPython, built for the browser. emscripten is the tool that compiled it to WebAssembly. On your computer the second line will be win32, darwin (macOS) or linux.
Expected output
Implementation: CPython
Platform: emscripten
Version: 3 13Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A syntax error on the last line stops the first line
Put a correct print first and a broken line last. Run it and look for the first line's output.
print("This line is fine")
print("This one is not"Break #2
A wrong name is only found when its line runs
Put a misspelled name inside an if that is never true, followed by a normal line.
if 1 > 2:
print(this_name_does_not_exist)
print("Finished without any error")Myth vs fact
Myth
Python is interpreted, so it never compiles anything.
Fact
CPython always compiles source to bytecode first. What it does not do (apart from the experimental JIT) is compile to the processor's machine code.
Myth
.pyc files make Python run faster.
Fact
They make it start faster by skipping recompilation. The bytecode that runs is the same.
Myth
Deleting __pycache__ will break my program.
Fact
It is only a cache. Python rebuilds it on the next run.
Myth
Python is slow, so my Python program will be slow.
Fact
Programs are slow where they spend their time. If that is inside NumPy, a database or a model API, the interpreter is not the limit.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
CPython is a stack-based virtual machine. Since 3.11 (PEP 659) the interpreter is specialising and adaptive: after an instruction such as
BINARY_OPhas seen the same types a few times, it rewrites itself in place into a specialised form (BINARY_OP_ADD_INT) with a cheap guard.dis.dis(f, adaptive=True)shows the specialised instructions after a function has warmed up. - ▸
The JIT added in 3.13 (PEP 744) is a copy-and-patch compiler working on the interpreter's micro-operations. It is off by default in 3.13 builds and still described as experimental; treat any speed-up as a bonus, and measure (Topic 13.7).
- ▸
.pycfiles are validated by the source file's modification time and size by default; hash-based.pycfiles (PEP 552) exist for reproducible builds.python -BorPYTHONDONTWRITEBYTECODE=1turns writing off, which is common in containers (a read-only image cannot be written to anyway). In this page's browser Python nothing is saved between visits, so a cache would not help. - ▸
Bytecode is an implementation detail with no stability promise: instruction names and layouts change in every minor version, and
.pycfiles from one version are not loaded by another. Tools that manipulate bytecode must be updated for each release.
Remember this
- 1
Step 1: compile. Python reads your whole file and translates it into bytecode: a list of small, simple instructions for an imaginary computer. This is when grammar is checked, which is why a
SyntaxErroranywhere stops the whole file before any line runs (Topic 0.7). - 2
Step 2: run. The Python virtual machine (the heart of the interpreter) goes through the bytecode one instruction at a time and carries each out: load this value, add these two, call that function. Exceptions such as
NameErrorhappen here, at the moment the failing instruction is reached. - 3
Both steps happen every time you type
python hello.py, automatically and in a fraction of a second. You never run a separate compiler as you would in C or Java. That is why Python is called an interpreted language, even though it does compile to bytecode internally. - 4
To save time, Python stores the bytecode of imported files in a folder named **
__pycache__**, in files ending in.pyc. Next time, if the source hasn't changed, it loads the.pycand skips step 1. You can delete__pycache__folders at any time; Python recreates them. Keep them out of version control. - 5
The Python from python.org is called CPython, because the interpreter is written in the C language. It is the reference: when people say "Python" they mean CPython. Other implementations run the same language in other ways: PyPy is built for speed with a just-in-time compiler, MicroPython fits tiny devices, and Pyodide, which powers this page's Run button, is CPython itself compiled to run inside a web browser.
- 6
Why Python is slower than C. The virtual machine does a lot of work for each small instruction: it checks what kind of values it has, finds the right operation, and creates a new object for the result (an object is a value stored in memory; Topic 1.1). Compiled C turns
a + binto roughly one processor instruction. The designers have been closing the gap. Python 3.11 made the virtual machine specialise itself for the types it actually sees. Python 3.13 added an experimental JIT (just-in-time compiler) that turns hot bytecode into machine code.
Explain it without notes
What are the two steps Python performs when you run a file?
Why does a syntax error on the last line stop the first line from running, while a NameError on the last line does not?
What is CPython, and how does it relate to PyPy and Pyodide?
What is in __pycache__, and what happens if you delete it?
Give two reasons pure Python code runs slower than C, and two ways Python programs avoid the problem.
Practice
Use compile() and co_consts to show what the compiler stores for the source "x = 60 * 60 * 24".
Write a program that prints Running on CPython if platform.python_implementation() is "CPython", and Running on something else otherwise.
A file has a SyntaxError on line 50 and a print("start") on line 1. What appears when you run it, and why?
Trade-offs
- ↔
Compile-at-run vs compile-ahead. Python needs no build step, so you edit and rerun in a second, and the same file runs on any system with an interpreter. The price is slower execution and errors that surface only when a line runs.
- ↔
CPython vs PyPy. PyPy can run long, loop-heavy pure-Python code several times faster, but the AI and data libraries are built around CPython's C interface and work best, or only, there. That is why almost everyone uses CPython.
- ↔
Dynamic checks vs speed. Looking everything up at run time makes Python flexible (you can change almost anything while the program runs) and makes each operation cost more.
Done when you can
Done when you can describe the path from source code to bytecode to the virtual machine.
Done when you can explain which errors come from the compile stage and which from the run stage.
Done when you can say what CPython is and name two other ways the language is run.
Done when you can explain what
__pycache__is for and why deleting it is safe.Done when you can give two reasons Python is slower than C and two ways around it.