Command Palette

Search for a command to run...

Hectal
PHASE 0Beginner ~28 min· topic 8 of 8

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 .py file.
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 python program: 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 .pyc files, 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.

From file to outputdiagram
Rendering diagram…

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.)

main.pywhole filepython
source = "print(2 + 3)"
code = compile(source, "tiny.py", "exec")

print(type(code).__name__)
print(code.co_consts)
print(code.co_names)
exec(code)
terminal
$ python main.py
── expected output ──
code
(5, None)
('print',)
5

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.

main.pywhole filepython
import dis

def add_tax(price):
    return price + price * 0.18

dis.dis(add_tax)
terminal
$ python main.py
── 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_VALUE
This is Python 3.13's bytecode. Instruction names change between versions; never depend on them.

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.

terminal
$ ls __pycache__
── expected output ──
helper.cpython-313.pyc

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.

Interpreted or compiled?diagram
Rendering diagram…

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.

main.pywhole filepython
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. 1

    Watch the compiler do arithmetic

    In the first example, change the source to "print(10 * 10 + 1)". Predict what co_consts will contain before running. The compiler folds the whole constant calculation into one number.

  2. 2

    Change the bytecode

    In the second example, change the function body to return price * 2. Run it and compare: fewer instructions, and a different LOAD_ line. Then try return price + 1.

  3. 3

    Find a __pycache__ folder

    On your computer, create helper.py containing GREETING = "Hello" and main.py containing import helper and print(helper.GREETING). Run python main.py, then list the folder. Delete __pycache__ and run again: it comes back.

    terminal
    $ python main.py
    ls
    ── expected output ──
    Hello
    __pycache__ helper.py main.py

Code & diagrams

Compile, then run: the two steps by hand New tab

The source says 2 + 3 but the compiled constants contain 5: the compiler did the sum in advance.

Sign in to run this example in your browser.

Expected output

code
(5, None)
('print',)
20 bytes of bytecode
5
Look at bytecode with dis New tab

Six small instructions for one line of Python. Change the function to return price * 2 and run again to see the bytecode change.

Sign in to run this example in your browser.

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_VALUE
Which Python is running this page? New tab

CPython, 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.

Sign in to run this example in your browser.

Expected output

Implementation: CPython
Platform: emscripten
Version: 3 13
Compile stage vs run stagediagram
Rendering diagram…

Break 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.

main.pywhole filepython
print("This line is fine")
print("This one is not"
terminal
$ python main.py
── what you'll see ──
File "main.py", line 2
print("This one is not"
^
SyntaxError: '(' was never closed

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.

main.pywhole filepython
if 1 > 2:
    print(this_name_does_not_exist)
print("Finished without any error")
terminal
$ python main.py
── what you'll see ──
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_OP has 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).

  • ▸

    .pyc files are validated by the source file's modification time and size by default; hash-based .pyc files (PEP 552) exist for reproducible builds. python -B or PYTHONDONTWRITEBYTECODE=1 turns 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 .pyc files from one version are not loaded by another. Tools that manipulate bytecode must be updated for each release.

Remember this

  1. 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 SyntaxError anywhere stops the whole file before any line runs (Topic 0.7).

  2. 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 NameError happen here, at the moment the failing instruction is reached.

  3. 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. 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 .pyc and skips step 1. You can delete __pycache__ folders at any time; Python recreates them. Keep them out of version control.

  5. 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. 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 + b into 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

01

What are the two steps Python performs when you run a file?

02

Why does a syntax error on the last line stop the first line from running, while a NameError on the last line does not?

03

What is CPython, and how does it relate to PyPy and Pyodide?

04

What is in __pycache__, and what happens if you delete it?

05

Give two reasons pure Python code runs slower than C, and two ways Python programs avoid the problem.

Practice

01

Use compile() and co_consts to show what the compiler stores for the source "x = 60 * 60 * 24".

02

Write a program that prints Running on CPython if platform.python_implementation() is "CPython", and Running on something else otherwise.

03

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.