How computers run code
Bits, memory, the processor, and what really happens when Python runs.
Why it matters
Every tool in this course, from a ten-line script to a large language model, runs on the same machine: a processor stepping through instructions and a long row of numbered memory slots. Once you can picture that machine, many puzzles go away. You'll see why one change shows up in two variables, why an API call is slow next to a loop, and why NumPy beats plain Python.
What you'll be able to do
- Explain how text, numbers and code all end up as bytes in memory
- Predict when two variables share one object, and avoid the bugs that causes
- Describe how a processor runs instructions, and how Python's interpreter fits on top
- Build a small stack machine, the same design Python's own virtual machine uses
What it covers
- Everything is numbers
- Memory: a long row of numbered boxes
- The processor: fetch, decode, execute
- A stack machine
- Count what the interpreter does
- Three beliefs worth correcting
Everything is numbers
Think of a row of light switches, each one either off or on. A computer stores everything that way, as bits: tiny switches that are off (0) or on (1). Eight bits make a byte, which can hold a whole number from 0 to 255.
Everything else is built from bytes, the way a paint-by-numbers picture is built from numbered colors. A photo is a list of color numbers. A song is a list of loudness numbers. Text is a list of character codes.
Python shows you the numbers behind text. ord gives a character's code, and encode turns a string into the bytes a file or a network would carry:
print(ord("A"), ord("a"), ord("€")) print(bin(65)) # 65 written in base 2: the bits themselves data = "café".encode("utf-8") print(data, len(data)) # é needs two bytes in UTF-8 print(data.decode("utf-8")) # and back again
The rule that maps characters to numbers is called an encoding. Think of it as a codebook both sides agree on. UTF-8 is the one the web uses. It stores plain English letters in one byte each, and other characters in two to four bytes. When text arrives scrambled, like café, someone decoded UTF-8 bytes with the wrong codebook.
In plain words: a letter, a color and a sound are all numbers to a computer. An encoding is the agreement about which number means what.
print(ord("A"), bin(5))
Show the answer
65 0b101
ord gives the character's number, 65, and bin writes 5 in binary with a 0b in front: 0b101. Letters and numbers are all stored as bits. Each character has a number.
Show the answer
6
Four plain letters take one byte each, and ï takes two: 6 bytes. Fonts only change how text looks, never how it's stored.
Memory: a long row of numbered boxes
Your computer's working memory (RAM) is like a long street of mailboxes. Each box holds one byte and has a number, its address. A laptop with 16 GB of RAM has about 16 billion boxes.
When Python runs prices = [4, 9], it builds a list object somewhere in memory. The name prices then points at it. A variable is a label tied to an object, not a box that holds a copy. id() shows which object a name points at.
That's why this surprises people:
a = [4, 9] b = a # b points at the SAME list, no copy is made b.append(12) print(a) # a changed too print(id(a) == id(b)) c = list(a) # list() builds a new list with the same items c.append(99) print(a, c)
Two names for one object is called aliasing. It's like one person being called both Bill and William: if William gets a haircut, so does Bill. It's often what you want, because passing a big list to a function shouldn't copy it. But when you mean "a separate copy", say so with list(x), x.copy() or dict(x).
Step through it. The function gets a second name for the same list, so its change is visible outside. The copy is a separate object.
x = {"n": 1}, y = x and y["n"] = 2, what is x["n"]?Show the answer
2
y = x makes a second name for the same dictionary. Changing it through y changes the one object both names point at.
The processor: fetch, decode, execute
The CPU (central processing unit) is the part that does the work. Picture an extremely fast, extremely literal cook, following a recipe one line at a time. It knows only tiny steps: load a number from memory, add two numbers, compare them, jump to another instruction. It repeats one cycle billions of times a second:
- Fetch the next instruction from memory.
- Decode it: which operation, on which numbers?
- Execute it, then move on to the next one.
Those instructions are machine code: numbers the chip understands directly. People don't write it by hand. Two kinds of program translate readable code into it, and they work like the two ways of translating a speech:
- A compiler is like translating the whole speech in advance and printing it. It translates the whole program ahead of time. C, Rust and Go work this way. The result runs fast, because the translation is already done.
- An interpreter is like a person translating live, sentence by sentence. It reads the program and carries it out as it goes. Python works this way.
Python sits in between. It first compiles your code into bytecode, a simple instruction list for an imaginary machine. Then a virtual machine (a loop written in C) reads that bytecode one instruction at a time. You can see the bytecode with the dis module:
import dis def total(price, qty): return price * qty + 5 for ins in dis.get_instructions(total): print(ins.opname, ins.argrepr)
Instructions starting LOAD_FAST put variables' values on the stack. LOAD_SMALL_INT puts the number 5 there. BINARY_OP pops two values and pushes the result, and RETURN_VALUE hands back whatever is on top. The exact names change a little between Python versions.
A stack machine
Python's virtual machine is a stack machine. Picture a stack of plates. Instructions push values on top, and operations take values off the top, combine them and push the answer back.
Here's 2 + 3 * 4 as stack instructions. Multiplication goes first, so its numbers go on first:
| instruction | stack afterward |
|---|---|
| PUSH 3 | 3 |
| PUSH 4 | 3, 4 |
| MUL | 12 |
| PUSH 2 | 12, 2 |
| ADD | 14 |
The whole machine is a loop and a list. That's the design you'll build in the exercise.
Why this matters: the speed ladder
Each step on this ladder is roughly a thousand times slower than the one before:
| operation | rough time |
|---|---|
| one CPU instruction | under a nanosecond |
| read from RAM | about 100 nanoseconds |
| read from an SSD | about 100 microseconds |
| network call to a server far away | about 100 milliseconds |
Those numbers are too small to feel, so stretch them. If one CPU instruction took a second, reading from RAM would take about two minutes, reading from an SSD about a day, and a call to a faraway server about three years.
So a loop over a million numbers can finish before one API call comes back. When an AI app feels slow, the model call is almost always the reason, not your Python.
The ladder also explains NumPy. A plain Python loop pays the virtual machine's cost for every item. NumPy hands the whole array to compiled code that runs the loop directly on the CPU.
Count what the interpreter does
Python can report how many bytecode instructions a function compiles to. More work per item means a slower loop.
import dis def one_step(x): return x + 1 def three_steps(x): y = x * 2 y = y + 1 return y - 3 for f in (one_step, three_steps): count = len(list(dis.get_instructions(f))) print(f.__name__, count, "instructions")
three_steps compiles to more instructions, and the virtual machine runs each one in turn. That's the interpreter's overhead. Compiled languages and NumPy avoid most of it by running machine code directly.
Show the answer
The model API call: a network round trip plus the model's work
A loop over 10,000 items takes milliseconds. A network call takes at least tens of milliseconds, and a model reply often takes seconds.
Three beliefs worth correcting
A variable doesn't hold its list. It holds a reference to where the list lives, like a contact card holding someone's address. That's why two names can point at one list, and changing it through one shows up through the other.
Python does compile. It turns your code into bytecode, then an interpreter runs that. The bytecode isn't machine code, which is a big part of why a Python loop is slower than one in C.
A slow app rarely needs a faster language. You wouldn't buy a faster car to shorten a three-year wait for a letter. Look at the speed ladder again. A network call costs millions of times what an addition costs, so waiting for a model or a database usually dwarfs everything your own code does.
Practice what you just read
- 4 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 6 review cards for spaced repetition
Try a quiz question
What does a compiler do that an interpreter doesn't?