Computing Decoded

Python Bytecode Explained: What Really Runs Your Code

See what Python does with a function: tokens, AST, bytecode, the stack machine and the .pyc cache, with real dis output and a free PDF.

Free PDF guide9 pages · diagrams, code, glossary

Free, 9 pages. The email is optional: leave it empty and the PDF downloads anyway. If you add it, I send new guides and nothing else. Privacy

Preview of the PDF guide: Python Bytecode Explained: What Really Runs Your Code

Python never runs your code as you wrote it. Your processor cannot read Python, so something in the middle has to translate it. This guide follows one tiny function all the way down, using CPython, the reference implementation, which Guido van Rossum started in the late 1980s.

def total(price, qty):
    return price * qty + 5

The pipeline

source text → tokens → syntax tree (AST) → bytecode → virtual machine

Everything here is real output from Python 3.9.6 and 3.14.7, measured on one Mac on 2026-11-02.

The pipeline from source text to tokens, syntax tree, bytecode and the virtual machine

Tokens

First, the tokenizer cuts the text into pieces. These are the real tokens in the return line:

NAME   'return'
NAME   'price'
OP     '*'
NAME   'qty'
OP     '+'
NUMBER '5'

Even indentation becomes tokens, which mark where a block starts and where it ends. You can see them with the tokenize module.

The syntax tree

Next, the parser builds a tree that follows the rules of maths. Multiplication sits deeper, so it happens first. This is the AST, the abstract syntax tree:

import ast
print(ast.dump(ast.parse("price * qty + 5", mode="eval").body))
# BinOp(left=BinOp(left=Name(id='price'), op=Mult(), right=Name(id='qty')),
#       op=Add(), right=Constant(value=5))

(Newer versions also print ctx=Load() on names; it is omitted here for readability.) If the syntax is wrong, Python stops at this stage, before it runs a single line.

The syntax tree of price * qty + 5

Bytecode

Then the compiler walks the tree and writes instructions. This is bytecode. On Python 3.9.6, dis.dis(total) shows six instructions:

Offset Instruction Argument
0 LOAD_FAST 0 (price)
2 LOAD_FAST 1 (qty)
4 BINARY_MULTIPLY
6 LOAD_CONST 1 (5)
8 BINARY_ADD
10 RETURN_VALUE

Each instruction takes exactly two bytes, so the function is 12 bytes: 7c00 7c01 1400 6401 1700 5300 in the raw co_code. LOAD_FAST takes a slot number: zero is price, one is qty. LOAD_CONST 1 means the constant at index 1, which is 5. In the raw bytes, a name is just a number.

Free maths: constant folding

The compiler also does some maths for you. Write 2 * 60 * 60 + x and the bytecode contains LOAD_CONST 7200, already multiplied. This is constant folding: decided at compile time, free at run time.

Locals and globals

Names live in different places. Arguments and local variables sit in numbered slots, which are fast to read (LOAD_FAST). A name like len is global, so Python looks it up by name (LOAD_GLOBAL). Local variables are one reason code inside a function often runs faster than code at the top level of a module.

The stack machine

A virtual machine runs the instructions one at a time, using a stack. With price = 3 and qty = 4:

Instruction Stack after it
LOAD_FAST price 3
LOAD_FAST qty 3, 4
BINARY_MULTIPLY 12
LOAD_CONST 5 12, 5
BINARY_ADD 17
RETURN_VALUE returns 17

The stack never held more than two values, which matches co_stacksize == 2. That is all a virtual machine is: a small program that pretends to be a processor.

The stack after each instruction while total(3, 4) runs

Loops are jumps

A loop has no special machinery either. for asks for the next value with FOR_ITER, and jumps out when there are none left; a jump instruction sends execution back to the top, again and again. A while loop works the same way, with a test and a jump.

Under the hood, the machine itself is a loop written in C. In simplified form:

for (;;) {
    instruction = next_instruction();
    switch (opcode(instruction)) {
        case LOAD_FAST:  push(local(arg)); break;
        case BINARY_ADD: { b = pop(); a = pop(); push(a + b); break; }
        case RETURN_VALUE: return pop();
    }
}

This is an illustration, not the real CPython source.

The cache: .pyc files

Compiling on every run would be wasteful, so Python saves the bytecode in a .pyc file, inside a folder called __pycache__. One detail: Python only saves this cache for imported modules, not for the script you run directly.

For our function, 50 bytes of source became a 276-byte file on Python 3.9.6. The first 16 bytes are a header:

610d0d0a 00000000 0672e86a 32000000
magic    flags    mtime    source size (0x32 = 50)

The header stores the source’s modified time and its size. If the source changes, Python compiles again.

The 16-byte header of a .pyc file

Bytecode changes between versions

Bytecode is not a stable format. The documentation says it is an implementation detail of CPython and promises no stability between versions. The same function on Python 3.14.7 looks different:

RESUME
LOAD_FAST_BORROW_LOAD_FAST_BORROW  price, qty
BINARY_OP *
LOAD_SMALL_INT 5
BINARY_OP +
RETURN_VALUE

Two loads are fused into one, and the arithmetic uses a single generic BINARY_OP. I timed 5,000,000 calls of total(3, 4) with timeit:

Python 5,000,000 calls
3.9.6 0.559 s
3.14.7 0.423 s

One machine, one run: treat it as a demonstration, not a benchmark. Never rely on bytecode in your own programs.

Faster Python: the adaptive interpreter

Since Python 3.11, the machine adapts while it runs (PEP 659). It watches which types an instruction sees, and swaps in a specialized version. After a few runs, BINARY_OP * on two integers can become BINARY_OP_MULTIPLY_INT. That is one reason the bytecode keeps changing between versions. You can see the specialized instructions with dis.dis(total, adaptive=True) after calling the function a few times.

See it yourself

import dis
def total(price, qty):
    return price * qty + 5

dis.dis(total)
print(total.__code__.co_code.hex())   # the raw bytes
print(total.__code__.co_consts)       # constants
print(total.__code__.co_stacksize)    # maximum stack depth

Cheat sheet

  • Source → tokens → AST → bytecode → virtual machine.
  • LOAD_FAST = local slot, LOAD_GLOBAL = lookup by name, LOAD_CONST = constant.
  • Two bytes per instruction. The stack machine works on a stack of values.
  • .pyc files cache imported modules; the header holds a magic number, flags, modified time and source size.
  • Constant folding does maths at compile time. Bytecode is not stable across versions.

Sources

Glossary

Bytecode
The low-level instructions CPython compiles your code into. It is an implementation detail and changes between versions.
Tokenizer
The first stage: it cuts the source text into tokens such as names, operators and numbers.
AST
Abstract syntax tree: a tree that follows the rules of the language, built by the parser from the tokens.
Virtual machine
A program that runs bytecode one instruction at a time, pretending to be a processor.
Stack machine
A machine whose instructions push values onto a stack and pop them to compute.
Constant folding
Doing maths on constants at compile time, so 2 * 60 * 60 becomes 7200 in the bytecode.
.pyc file
The cached bytecode of an imported module, kept in a __pycache__ folder.
LOAD_FAST
The instruction that loads a local variable from its numbered slot.
Adaptive interpreter
Since Python 3.11 the machine swaps in specialized instructions after seeing which types they handle.

Practice

Try these before opening the answers.

Run dis.dis on def f(a): return a + 1. Which instruction loads the number 1?

LOAD_CONST on Python 3.9 and LOAD_SMALL_INT on Python 3.14. The idea is the same; the name changes between versions.

What does constant folding do to 2 * 60 * 60 + x?

The compiler computes 2 * 60 * 60 = 7200 itself, so the bytecode loads the constant 7200 and adds x.

Why does Python not save a .pyc for the script you run directly?

It only caches imported modules. The script you run is compiled each time.

How can you see the largest number of values the stack ever holds in a function?

Read code.co_stacksize, for example total.__code__.co_stacksize. For total it is 2.

Frequently asked questions

Is Python compiled or interpreted?

Both. CPython first compiles your source to bytecode, then a virtual machine interprets that bytecode. The compile step is automatic and usually invisible.

What is a .pyc file?

It is the cached bytecode of an imported module, stored in a __pycache__ folder. It has a small header, and by default Python compares the source's modified time and size with the header to decide whether to compile again.

Can I rely on bytecode in my own programs?

No. The documentation says bytecode is an implementation detail of CPython and that instructions can be added, removed or changed between versions.

How do I see the bytecode of a function?

Import the dis module and call dis.dis(your_function). It prints one line per instruction.

Keep this guide: free PDF

9 pages with the diagrams, the code, a glossary and practice questions, ready to print or save.

Free, 9 pages. The email is optional: leave it empty and the PDF downloads anyway. If you add it, I send new guides and nothing else. Privacy

New guides, straight to your inbox

One email when a new video and guide come out. No spam, no tracking pixels.

More guides