python/volume-1/chapter-00ch 00 / 64 · 12 min

00From the metal up

You came here to write Python, and we will — but not on page one. First I want to open the box. Python feels like magic right up until you can see what's under it, and then it feels like machinery — which is far better, because machinery you can reason about. Here's the plan: we start at a single switch, the transistor that's either on or off, and we climb — one bit to a byte, bytes to RAM, RAM to the CPU that turns out to be the only part of the whole box that actually does anything. And the whole way up we keep asking the one thing most tutorials skip right past — what really happens between saving a .py file and the instant it runs? By the end you'll read a binary number cold, picture memory as a street of numbered boxes, and trace your own code all the way down to the metal. Everything in the next fourteen chapters stands on this floor.

bits & bytes RAM as numbered boxes the CPU's fetch loop stack vs heap
scroll to begin

01A computer is a box of switches

Let's start with the smallest real thing in the machine. Take away the screen, the keyboard, the glowing logo, and what's left is a box of switches — billions of them. Each switch is a transistor: a tiny piece of silicon that either lets electric current through or blocks it. That's all it can do. There's no third setting, no "half-on," no "maybe." Just off or on. We give these two states names — 0 and 1 — and one switch's on-or-off state is called a bit, short for binary digit. A bit is the smallest piece of information that exists: one wire, either carrying voltage or not. You can't split information any smaller than that.

Here's the thing to sit with. Everything your machine "knows" — this sentence, a photo of your grandmother, your favourite song — is, underneath everything, just a pattern of ons and offs. Nothing else is in there. And that raises a problem this whole chapter is built around. Take one small, real fact from a playlist we'll use throughout the book: the song "Blinding Lights" by The Weeknd runs 200 seconds. Now try to store that using switches. A single switch can only say yes or no. It can't say 200. So right away, on the very first fact we care about, one switch looks far too small for the job. Keep that problem in mind — solving it is the whole trick behind digital computing.

THE ONE IDEA TO CARRY FORWARD
A bit has exactly two states, and a computer has only bits. Every number, letter, colour and sound you will ever see a computer handle is built by lining up more of these two-state switches. There is no other ingredient. The rest of this chapter is just watching that one idea grow.
One transistor, two statesfigure
switch open — current blocked bit = 0 switch closed — current flows bit = 1
Fig 01 — One transistor, two states: current blocked reads as 0, current flowing reads as 1. That is a bit — the atom of all data. Everything above this page is built from copies of exactly this.
Wait — why two states and not ten, so a switch could just hold a digit 0–9 directly? Engineers tried. The trouble is telling the levels apart reliably: with two states you only have to distinguish "clearly off" from "clearly on," and a little electrical noise never flips one into the other. Ten voltage levels crammed onto one wire would smear into each other the moment the chip warmed up. Two states is not a limitation the universe forced on us — it's the most robust choice, and robustness at billions of switches is everything.
↺ reframe
Don't picture a computer "understanding" 200. Picture a row of light switches on a wall. Each is just up or down. The machine never sees the number — it only ever sees which switches are up. Any meaning is a pattern we agreed on beforehand, not something the switches themselves contain.
The deeper cut — how one switch became two, and why "off or on" is a useful lie

Saying a transistor is simply "off or on" is a labelled simplification — true enough to build your whole mental model on, and we'll refine it exactly once, here. In reality the voltage on that wire is a smooth, continuous quantity that can sit anywhere between the ground rail and the supply rail. What makes it behave as a clean 0 or 1 is a deliberate design choice: the circuit treats everything below a low threshold as "0," everything above a high threshold as "1," and the ambiguous middle band as forbidden — a no-man's-land the signal is engineered to snap out of within nanoseconds. So the switch is analogue underneath; the two-state behaviour is imposed on top of it. This is why the digital abstraction is so sturdy: even if a real 1 arrives a bit droopy, as long as it lands above the threshold it is read as a perfect 1, and the droop never accumulates. That single trick — quantise, then round — is what lets a billion imperfect physical switches compute a flawless answer. Everywhere else in this book we'll happily say "off or on," now that you know what's really underneath.

So the basic unit is settled: one switch, two possibilities. Everything after this is just combination. Two switches together don't give you three possibilities — they give you four (off-off, off-on, on-off, on-on). Three switches give eight. Every switch you add doubles the count — and it's that doubling, not any single switch, that will eventually be enough to hold a number like 200.

the bit
One switch, two states — 0 or 1. The smallest unit of information that can exist.
the size problem
A single bit can't hold 200. That mismatch is what the rest of the chapter resolves.
two states, on purpose
Two-state switches are what's reliably distinguishable at billions of copies — not a limitation, a choice.
doubling, not adding
Each extra switch doubles the count of possibilities — that's the trick that eventually reaches 200.

One switch stores exactly two possibilities. So how many switches does 200 need — and arranged how? →

02Counting with two fingers

You already know this trick — you just know it in base ten. Read 743 from right to left: 3 ones, 4 tens, 7 hundreds. Each column is worth ten times the column to its right, and each digit just says how many of that column to count. That's really all a number is — a row of columns with fixed values, and digits saying how many of each to take.

Binary plays the exact same game, just with two instead of ten. Each column is now worth double the column to its right — …8, 4, 2, 1 — and since a binary digit can only be 0 or 1, it stops meaning "how many" and simply means in or out. And that's the part worth noticing: in or out is the one and only thing a transistor can ever report — current is flowing, or it isn't. Ten separate voltage levels would be hard to tell apart reliably; two levels just need "clearly below the line" or "clearly above it." So binary isn't some strange dialect computers were forced into — it's the richest counting system you can build on a switch. Read 1011 this way and it means 8 + 0 + 2 + 1 = 11. Same place-value idea you learned in school, just with a smaller alphabet.

Wait — why "two fingers"? Because if all you can do is raise or lower one finger, you can still count as high as you like by lining up more fingers and giving each one double the weight of the last. Ten fingers held this way count past a thousand. A byte is eight such fingers.
Reading 1011 column by columnfigure
2⁰ 8 4 2 1 1 0 1 1 8 0 2 1 + + + 8 + 0 + 2 + 1 = 11
Fig 02 — Binary is place value with doubling columns: 1011 is 8+0+2+1 = 11. No new math — just base 2, where a lit column contributes its value and a dark one contributes nothing.

✗ The myth

"Computers think in ones and zeros" — some alien logic, fundamentally unlike how humans count.

✓ The reality

It's ordinary place value with two symbols instead of ten. The only reason it looks foreign is the small alphabet; the arithmetic underneath is the same doubling-columns idea you already trust in base ten.

Group 8 bits together and you get a byte — the standard unit computer memory is measured in, and the block everything else is built from. Why does one byte max out at 256 patterns? Just count them. One switch gives 2 possibilities (off, on). Add a second switch and every existing pattern splits in two — new switch off, or new switch on — so the count doubles every time you add a switch: 2, 4, 8, 16, 32, 64, 128… By eight switches, that's 2⁸ = 256 distinct patterns — exactly enough to count from 0 to 255. (256 patterns, but the first one is zero, so the highest is 255, not 256 — worth remembering.)

place value, base 2
Each binary column is worth double the one to its right — same idea as base ten, smaller alphabet.
the byte
8 bits = 256 patterns = the standard unit memory is measured in.
a shape, not a number
The same 8 switches mean different things depending on how you agree to read them.

And here's where our playlist fits in. "Blinding Lights" runs 200 s, and 200 = 128 + 64 + 8 = 11001000 — so one byte is enough to hold the song's length, with room left over. "Titanium," at 245 s, is 11110101 — still just one byte. One honest clarification: the song itself — the actual audio — takes up millions of bytes. The one small number that fits in a single byte is just the song's length in seconds, not the sound itself. But those millions of bytes do have a shape: the audio is a waveform — a long run of samples, where each sample is a plain number describing where the speaker cone sits at that instant. A song's length is a small fact a human typed in once; its samples are the real substance the playlist is built from, and we'll come back to this waveform later in the book. Try the slider below — drag it and watch the eight switches spell out whatever number you land on. (Later on, Python even lets you flip individual bits by hand using operators like << — you'll see this in chapter 4.)

One byte — flip 8 switches, make any number 0–255interactive
128 64 32 16 8 4 2 1 1 1 0 0 1 0 0 0 128 + 64 + 8 = 200 0b11001000
200
Each switch is worth double its right-hand neighbour — 8 of them cover every number from 0 to 255. Find 200 (the length of "Blinding Lights"), then 245 ("Titanium"), then 255 (all switches on), then 1 (just the last one).
Fig 02b — The lit (green) switches always sum to the number on the slider — that sum is the number, read in base 2. The cyan line is how Python would print it: 0b plus the eight bits.
↺ A byte isn't a number — it's a shape
The same eight-switch pattern 11001000 is only "200" because we decided to read it as a plain count. Feed the very same switches to a different agreement and the meaning shifts underneath you: the pattern 01000010 is the number 66, and 66 is the code the whole system agrees means the capital letter 'B' — Python will hand it straight back with chr(66). Read one byte as a colour channel instead and it's a dim shade of grey. The bits are only in/out; meaning is the interpretation we lay on top. Keep that in your pocket — it's why one file format can't open another's bytes.
One sample is two bytes — the waveform's turn
Take a single instant of "Blinding Lights". The speaker-cone position at that instant is one number — say 24000 — and, exactly like the byte above, that number is a row of switches: 0b0101110111000000, sixteen of them. Same bits; "a sound sample" is only the reading we lay on top, just as 0b01000010 was either 66 or 'B'. But look at the size — 24000 > 255, so eight switches can't reach it. This one sample needs a 2-byte team: a signed 16-bit integer, range -32768 to 32767, with 0 = cone at rest. There's your second concrete reason multi-byte teams exist — not the song's length this time, but a single one of its millions of samples. Python packs a run of them as array('h', [0, 12000, 24000, 18000, -6000, -22000, -14000, 3000]) — the 'h' means two bytes each — and 24000 is the loudest sample in that run.

You can check this against Python directly. bin() turns a number into its switch pattern, and typing a binary literal turns it back into a number — no magic, just the same base-2 place value the slider above just showed you:

bits.pypython
# a number is just how we read a pattern of switches
print(bin(200))        # 0b11001000  — "Blinding Lights", 200 s
print(bin(245))        # 0b11110101  — "Titanium", 245 s
print(0b11001000)      # 200 — Python reads the switches back
print(8 + 0 + 2 + 1)     # 11 — the 1011 from Fig 02
print(bin(24000))      # 0b101110111000000 — one sample: 15 bits, too big for a byte → 2-byte 'h'
You've been reading bytes for years
Ping a server and it answers "64 bytes from 216.239.38.120" — and look hard at that address: four numbers, every one landing between 0 and 255, because each dotted octet is exactly one byte (that's the whole reason an IP number never reads 256). A high-quality MP3 of "Blinding Lights" is roughly eight million bytes — and that's the compressed file; unpacked to its raw waveform the same song is about 8.8 million samples × 2 bytes ≈ 17 MB of switch patterns. Your phone's storage, that email attachment's size, the "500 MB left" warning — all of it is priced in this one unit. The byte is the atom everything else on a computer is measured in.
The deeper cut — why 8, and where hex comes from

Eight bits per byte wasn't handed down by nature; it was a design choice that won. Early machines used 6-, 7-, and 9-bit groupings, but 8 hit a sweet spot: enough patterns (256) to cover every letter, digit, and punctuation mark of early text with room to spare, and a clean power of two that hardware likes.

Writing bytes as eight 0s and 1s is exhausting to read, so programmers group the byte into two halves of four bits and give each half a single symbol — 0–9 then af for the values ten through fifteen. That's hexadecimal. One byte becomes two tidy characters: 11001000 is c8, and Python writes it 0xc8. It's the same byte, same 200 — just a denser handwriting. You'll see hex everywhere colours are written, like #5ede78 (that green is three bytes: red, green, blue).

A byte maxes out at 255, and a playlist is thousands of bytes of titles and lengths. Where do they all live, and how does the machine find the right one? →

Trace3 step the machine — idea · code · memory move together
Building a byte — how 200 becomes 11001000
Read the number one column at a time, biggest place-value first. Take the column if it fits in what's left. Press Next and watch the switches, the code and the memory move as one.
8 columnsbase 20–255
The idea — eight switches, biggest firstbyte · 8 bitsinit
In plain words
Under the hood
SETALU · the operation
Program · to_byte.py
variables · type
Memory · registers + object
registers — the running state
bits str · the byte so far
what's happening
beat 1 / 1

03RAM: a street of numbered boxes

RAM (random-access memory) is where a running program keeps the data it's working with right now. The honest picture is almost embarrassingly simple: one enormous row of byte-sized boxes, and each box has a serial number called its address. Box 0, box 1, box 2, and so on — an 8 GB machine has about 8.6 billion of these boxes in one unbroken line. The important word is random-access: you can open box 5,000,000 exactly as fast as box 5. You don't walk down the row counting doors — you're simply given the address, and you're there. Same cost, one jump, no matter where in the line.

Store our song and this becomes concrete. The title "Blinding Lights" is fifteen characters long — fourteen letters plus the space between the two words — so it takes up fifteen boxes, one character per box. Even the space is stored as a byte: it holds the number 32. And the box holding the first letter, B, doesn't actually store the shape of the letter — it stores the number 66, a code the whole system has agreed means "B" (more on exactly which code, and why, in chapter 2). The length, 200, fits in one more box, since it's under 255. Anything too big for a single byte — past 255 — needs a team of boxes working together, usually 4 or 8 acting as one wider box. Store a whole list of song lengths this way and their addresses count up like house numbers on a street: 6000, 6004, 6008…, always four apart. That even spacing is the whole trick: to find length #3, the machine doesn't need to search for anything. It just computes start + 3 × 4 = 6012 and jumps straight there. Arithmetic, not hunting.

random-access
Every box is one hop away — box 5,000,000 costs exactly the same as box 5.
address = arithmetic
Finding item #3 is start + 3 × stride, not a search through the street.
who owns the boxes
Your program never picks its own boxes — the operating system hands them out.

One thing worth being precise about: this is the raw-metal picture — the way C and NumPy pack a row of numbers side by side, wall to wall. Python's own list uses this same even-spacing trick, but with a twist: its evenly spaced slots don't hold the actual lengths — they hold 8-byte addresses pointing to where each length really lives, elsewhere in RAM. Same idea, just one extra step (chapter 6 covers this fully). One more thing to know: your program never picks its own boxes. The operating system hands each program its share of RAM and keeps track of who owns what.

"Blinding Lights" laid out in RAM, one byte per boxfigure
RAM — one long row of byte-sized boxes, each stamped with an address B 5000 l i n d i n g 32 5008 L i g h t s 5014 200 len 5015 one byte per character — even the space is a byte (code 32) · the length 200 rides in box 5015 · 8 GB ≈ 8.59 billion boxes
Fig 03 — "Blinding Lights" spelled across fifteen boxes (5000–5014) — one byte per character, the space included — with the length 200 in box 5015. Data has no home without an address.
↺ the address is the speed
"Random access" isn't about randomness — it's that every box is one hop away, so the machine can compute where a thing lives instead of scanning to find it. That single property is why an array lookup is instant and why "the 3rd item" costs the same as "the 3-millionth."
Drag the index — watch it compute the address, not search for itinteractive
a row of song lengths, each a 4-byte team · base address 6000, stride 4 #0 200 6000 #1 245 6004 #2 222 6008 #3 213 6012 #4 190 6016 #5 258 6020 #6 201 6024 #7 176 6028 6000 + 3×4 = 6012 → holds 213 s no scanning · the address is a one-line calculation
#3 → box 6012
Slide the index. The highlighted box is never found — its address falls straight out of 6000 + i × 4. That's the difference between an array (instant) and rummaging (slow).
Fig 03b — Even stride turns "the i-th item" into a multiplication. This is the beating heart of every array, list, and NumPy row you'll ever touch.
RAM FORGETS
RAM is volatile: cut the power and every box blanks in an instant. That's why unsaved work dies with a crash — it never left RAM. Your SSD is the opposite: persistent but far slower, so "loading" a file just means copying its bytes from SSD boxes into RAM boxes, and "saving" copies them back. The song only plays from RAM; it only survives on the SSD.
Wait — box 5002 holds 105 (the letter "i") and box 5015 holds 200 (a length in seconds). Both are just numbers sitting in identical boxes. How does the machine know which is a letter and which is a duration?

It doesn't know. A box just holds a number — no label, no unit, nothing painted on the side to say what it means. The meaning comes entirely from the program reading it: the same number 200 could be a letter-code, a song length, or a shade of grey, depending only on what the code decides to do with it. Keep that in mind — it grows into Python's type system in chapter 2, which really just comes down to keeping track of what each box was meant to represent.

WHY 4 OR 8, NEVER 3
The teams come in tidy powers of two (1, 2, 4, 8 bytes) so the stride stays a clean multiple and start + i × stride is a single fast multiply. Pick an odd team size and every lookup would need awkward arithmetic — the hardware would rather waste a byte of padding than lose the even stride.
The deeper cut

Real tools write addresses in hexadecimal — base 16, digits 0–9 then A–F. The reason is exact, not cosmetic: one hex digit is precisely 4 bits, so one byte is always two hex digits. That's why 255 = 0xFF and our address 5000 = 0x1388 — hex lets you read the raw bits at a glance where decimal blurs them.

And a second layer you'll eventually trip over: the addresses your program sees are virtual, not the real chip locations. Each program gets a private, pretend address space that starts fresh, and the MMU (memory-management unit, hardware sitting right beside the CPU) translates virtual to physical on every single access. That's how two programs can both believe they own address 5000 without a fistfight — their "5000"s quietly map to different physical boxes.

a box has no label
The same number means different things depending only on how the reading program treats it.
addresses in hex
One hex digit is exactly 4 bits, so a byte is always two hex digits.

Billions of boxes, patiently holding numbers. But boxes don't add, compare, or copy themselves. Who does the actual work? →

Trace3 step the machine — idea · code · memory move together
Random access is arithmetic — how row[3] becomes address 6012
This is the raw-array model — the way C and NumPy pack numbers wall-to-wall, each value sitting inline in its own slot (a Python list adds one more hop, its slots holding addresses instead — §3). Laid out inline like this, reading item i never searches: the machine computes base + i × stride in one shot and jumps straight to that box. Press Next and watch the address get built.
8 slotsstride 4O(1)
The idea — one contiguous block in RAMlist · 8 × 4Binit
addr = base + i × stride
In plain words
Under the hood
SETALU · the operation
Program · address_of.py
variables · type
Memory · registers + object
registers — the running state
row list · 8 ints, 4 bytes each — one contiguous block
what's happening
beat 1 / 1

04The CPU: the only part that does anything

Everything else in the machine just holds data or moves it around. The CPU — the central processing unit — is the one part that actually does something with it. It's the machine's single busy worker, and its whole job is one repeating loop: the fetch–decode–execute cycle. Fetch the next instruction from RAM, decode what it means, execute it — then start again, over and over, until the power turns off. Each instruction is tiny: copy one box into the CPU, add two values, compare two values, write a box back out. The hardware never "understands" your program in any deeper sense — there's only this loop, spinning. (One simplification worth flagging: a real CPU core doesn't actually finish one fetch–decode–execute before starting the next one — it runs several in flight at once, called pipelining, and can even do a few per tick, called superscalar execution. Thinking of it as one instruction at a time is still the right mental model — just don't take the "four instructions ≈ one nanosecond" estimate below as four instructions run strictly one after another.)

Here's the constraint that makes the next piece of hardware necessary: the ALU can't reach into RAM and add two boxes where they sit — it can only work with values already inside the CPU. So before it can add anything, those values have to be copied in first. That's exactly what registers are for: a small number of private scratch cells built right into the CPU — the only place where arithmetic can actually happen. They're also much faster than RAM, for a simple reason: they're etched directly onto the CPU chip, so a value in a register never has to travel down a wire to fetch it — it's already there. The actual adding and comparing happens in a dedicated circuit called the ALU (arithmetic-logic unit), which reads from the registers and writes results back to them. And a quartz clock keeps this whole loop running at roughly 4 GHz — four billion cycles a second, one tick every quarter of a nanosecond.

WHY REGISTERS BEAT RAM
It's pure distance. RAM is a separate chip; every read crosses a bus (the shared wires between CPU and memory). A register is on the CPU die, microns from the ALU. Same idea as keeping the two numbers you're adding in your hand versus walking to a filing cabinet for each one. Registers are tiny — a modern CPU has only a few dozen — so they hold just the values in active use this instant.

Let's watch the CPU total up our playlist — and watch a single byte actually run out of room. "Blinding Lights" (200 s) fits in one byte; "Titanium" by David Guetta (245 s) fits in one byte too. But add them together — 200 + 245 = 445 — and the result is bigger than 255, too big for any one byte to hold. That's exactly why the machine stores each song length in a 4-byte team (see §3) instead of a single byte: the extra room leaves space for totals like this. Here's how it plays out: the two lengths sit in RAM, the CPU loads both into registers, the ALU adds them, and the result, 445, gets written back to a third slot. Four instructions in total. Step through it below.

The one loop all hardware runsfigure
FETCH read next instruction from RAM DECODE work out what it means EXECUTE do it — add, copy, compare 4 GHz ≈ 4,000,000,000 cycles / second one cycle ≈ a quarter of a nanosecond
Fig 04 — One dumb loop, repeated four billion times a second, is all the "intelligence" the hardware has. Every clever thing a computer does is this cycle, run enough times.
Step the CPU — add two song lengthsinteractive
next instruction: — (drag the slider) CPU A · B · ALU — the adder circuit bus RAM 6000 200 Blinding Lights 6004 245 Titanium 6008 · total Two song lengths sit in RAM. The registers are empty scratch cells.
step 0 / 4
Every program you will ever run — including Python itself — reduces to steps this small. The green outline shows which box each instruction touches.
Fig 05 — Load, load, add, store. Four instructions turn two boxes in RAM into a third. That's a whole computation.
Wait — why does adding need four instructions when it's obviously "one" operation? Because the CPU can only add numbers that are already sitting in registers. It cannot reach into RAM and add in place. So two of the four steps are pure logistics — fetch the operands home — one is the actual add, and one ships the answer back out. Most of what a CPU does, by count, is moving values around so the small amount of real arithmetic has something to chew on.
FEEL THE SPEED
Those four instructions take the CPU about one nanosecond. In the time it took you to read this sentence — call it three seconds — a 4 GHz core could run on the order of twelve billion cycles. It could have totalled a playlist of a few billion songs. The reason your computer ever feels slow is never the adding; it's waiting — for the disk, the network, or a value stranded out in RAM.
The deeper cut: where does the CPU keep its place?

One register is special: the program counter (PC). It holds the RAM address of the next instruction to fetch. The "fetch" step reads whatever the PC points at; then the PC advances to the following instruction, and the loop repeats. That's the whole trick behind a program having an order.

It's also how a program can make a decision. A "compare" instruction can set the PC to jump somewhere else instead of just stepping forward — "if A is bigger than B, continue from address 7200." Loops and if-statements, all the way up to the branch in your Python code, bottom out as the CPU writing a new number into the program counter. There is no magic control flow underneath; there is only "which box do I read next."

Notice exactly what the CPU was following the whole time: short binary instructions like LOAD, ADD, and STORE, encoded as numbers and specific to this particular chip. It has never seen a single English letter. But you write text that looks like English in a .py file — something like 200 + 245. Something has to bridge that gap.

fetch–decode–execute
The CPU's whole life is one loop. There's no step where it "understands" your program.
registers, not RAM
Arithmetic only happens on values already inside the CPU — that's what registers are for.
the gap
The CPU only ever sees numbered instructions — never a letter of English.

You write English-ish text; the chip speaks numbered instructions. Someone has to translate — and that someone is the next piece of the machine →

Trace3 step the machine — idea · code · memory move together
The CPU only moves — adding 200 + 245 takes four steps
A CPU can't add two numbers while they sit in RAM. It must LOAD each into a register, ADD them in the ALU, then STORE the result back. Three of the four instructions just move values around — only one actually adds. Press Next.
4 instructions2 registers1 ALU
The machine — CPU · bus · RAM4 instructionsboot
CPU
A
B
ALUidle
bus
RAM
6000Blinding Lights200
6004Titanium245
6008total?
In plain words
Under the hood
IDLEALU · the operation
Program · add.asm
registers · type
Memory · registers + RAM
registers — the running state
RAM main memory · three cells
what's happening
beat 1 / 1

05The ladder from your text to the metal

A CPU only understands one language: its own machine code — raw binary instructions, and a different dialect for every chip family. Your laptop's x86 chip and your phone's ARM chip can both do arithmetic, but the exact byte patterns that mean "add these two numbers" are different on each — hand one chip the other's machine code and it's meaningless. What you actually write is source code: human-readable text that looks the same on every machine. Something has to translate between the two, and there are two classic approaches. A compiler translates the whole program into machine code once, ahead of time, and hands you a finished, runnable file — fast to run, since the translation work is already done (this is how C, C++, and Rust work). An interpreter is itself a running program that reads your code and carries it out on the spot, translating each piece right when it's needed. In its purest form, nothing gets saved, so it repeats that translation work every single run — slower overall, but it starts instantly with no separate build step. (Real Python is a bit more clever than this — it quietly saves some of its translated work, as you'll see in a moment.) Think of it like a novel translated once and printed forever (that's a compiler) versus a live interpreter at a summit, re-translating the same speech from scratch every time it's delivered (that's an interpreter) — and that repeated re-translation is exactly where interpreted programs spend their extra time. (A JIT is a hybrid that avoids choosing: it starts out interpreting, notices which parts of the code run over and over — the "hot" loops — and quietly compiles just those parts to machine code while the program is still running. That's the trick behind the JVM and V8, the engine that runs JavaScript in your browser.)

✗ The myth

You install Python and your CPU "learns Python." From then on the chip runs your .py file directly.

✓ The reality

The CPU only ever runs machine code — it never changes. python.exe is a pre-built machine-code program; your .py file is just data — text — that this already-running program opens, reads, and acts out. Deleting Python doesn't un-teach the chip anything; it just removes the actor.

Here's Python's actual pipeline, which is more layered than the single word "interpreted" suggests — because it quietly does both jobs. When you run a file, CPython first compiles your text into bytecode: compact, numbered instructions for a made-up, portable CPU that doesn't physically exist anywhere. This is called an intermediate representation — no longer your original text, but not yet your hardware's machine code either. Then CPython's virtual machine — a real program, written in C — behaves like that made-up CPU: it walks through the bytecode and interprets it, one instruction at a time. So there are two separate translation steps stacked on top of each other, and neither one is optional. Two terms are about to matter in the disassembly just below, so let's define them now. A function is a named mini-program: you call it to run it, and once it's done, it returns a value to whoever called it — the caller (you'll write your own functions in chapter 9). And that made-up CPU has a specific shape: it's a stack machine. Instead of the named registers from §4, it keeps one scratch pile called a stack, which you can only touch at the top — push a value on, pop one off, always last-in, first-out. Why a pile instead of registers? Because one simple push/pop pair can handle an expression nested to any depth, without needing a separate name for every intermediate value. It's the simplest possible engine that can still run any code at all.

The translation ladderfigure
SOURCE CODE — human-readable text (.py) song_seconds = 200 CPython compiles it — whole file, automatically BYTECODE — portable intermediate code LOAD_CONST 200 · STORE_NAME song_seconds the VM interprets it — one instruction at a time CPYTHON VIRTUAL MACHINE — a CPU made of software itself already machine code (compiled from C, long ago) which the real CPU executes HARDWARE CPU — machine code only, architecture-specific (x86, ARM)
Fig 05 — Python's real pipeline: compile to bytecode, then a software CPU interprets it. Two rungs, not one — the interpreter you keep hearing about is only the bottom half.
↺ The thing people get backwards
Most people think "compiled vs interpreted" is a property of the language — that Python simply is an interpreted language, the way a lemon is sour. It's really a property of the implementation, and CPython does both: it compiles your file to bytecode, then interprets that bytecode. (Other implementations choose differently — PyPy JIT-compiles the hot parts, and the same Python source could in principle be fully compiled ahead of time.) So "Is Python compiled or interpreted?" has a genuinely correct one-word answer: yes.
You can see the bytecode yourself
Python ships a disassembler in the standard library — nothing to install. Once chapter 1 has you running files, feed a function to dis.dis and read the rungs. Every line it prints is one tick of the virtual machine: the exact instructions the VM will walk, in order, when that function runs.
disassemble.pypython
import dis

def total_seconds(levels, titanium):
    return levels + titanium

dis.dis(total_seconds)
# prints the function as VM instructions:
#   LOAD_FAST   levels     ← push Blinding Lights (200) onto the stack
#   LOAD_FAST   titanium   ← push Titanium (245) on top of it
#   BINARY_OP   +          ← pop both, push their sum (445)
#   RETURN_VALUE           ← hand 445 back to the caller

Four instructions — and you've just seen the whole ladder in miniature. Your one line, return levels + titanium, became a short list of moves on a stack, and the VM runs them left to right. Call total_seconds(200, 245) and those four steps produce 445 — the full playlist, Blinding Lights plus Titanium, added together in seconds.

two translation rungs
Text → bytecode (compile) → run on the VM (interpret). Neither step is optional.
a stack machine
The bytecode VM has no registers — only a push/pop stack, touched at the top.
One line of Python — count what the metal actually doesinteractive
you write one line — return levels + titanium. how much does the metal actually do to run it? transistor switch-flips AT THIS RUNG, ONE LINE IS ~1 billion AND ONE OF THOSE IS a transistor flipping in/out ≈ the box of switches from §1, live AMPLIFICATION — YOUR ONE LINE, EXPLODED ×1,000,000,000 times the work of the line you typed. The cliff: ~1,000 instructions → ~a billion transistor flips — §1's switches, live.
the metal
Drag from your source line at the top down to the transistors at the bottom. The counts below the bytecode are order-of-magnitude estimates (hardware-dependent) — but the shape is real: one friendly line you typed becomes ~a billion in/out switch-flips at the metal. That is the price of every rung of comfort the ladder buys you.
Fig 05b — The same four bytecode ticks you just disassembled, followed the rest of the way down. Each rung the machine descends multiplies the work — and the bottom rung is nothing but §1's box of switches, flipping a billion times to keep your one-line promise.
The deeper cut
CPython caches compiled bytecode on disk so it doesn't have to recompile unchanged code every single run — that's the __pycache__ folder that appears next to your files, holding names like playlist.cpython-312.pyc. The number is the CPython version that produced it: bytecode is portable across operating systems (the .pyc from your Mac would run on a Linux server) but not across Python versions — a 3.12 .pyc means nothing to 3.13, so the version is stamped right into the filename. Machine code, for contrast, is portable across neither OS nor CPU family; bytecode's portability is exactly the payoff for adding that extra rung.

And the "made-up CPU" has a specific shape: it's a stack machine. It keeps a small scratch stack of values; LOAD_* instructions push values onto it, and operations pop their inputs off the top and push the result back — which is precisely the LOAD / LOAD / add / return dance you just disassembled. Hold onto that push-and-pop picture: the very same last-in-first-out discipline is about to reappear one level up, in how Python keeps track of function calls.

So a software CPU is executing your program. While it runs, it has to park thousands of values somewhere in those RAM boxes from the last section. It turns out the parking lot isn't one uniform space — it has two very different districts, with very different rules →

Trace3 step the machine — idea · code · memory move together
How a + b runs — four bytecode ticks on a value stack
A stack is a scratch pile you may touch only at the top — push a value on, pop one off, last-in-first-out. CPython compiles return a + b into bytecode for a made-up machine built around exactly one such pile: LOAD pushes a value, an operation pops its inputs and pushes the result. With a = 200 and b = 245, the four ticks leave 445 sitting on top — the very total the CPU trace computed a level down. Press Next and watch the stack grow and shrink as one.
stack machineCPython 3.12a=200 · b=245
The value stack — grows up as values pushlist · TOS on topRESUME
▲ pushes grow the stack upward · bottom below
In plain words
Under the hood
BOOTALU · stack effect
Bytecode · total_seconds() compiled from: return a + b
locals · type
Memory · value stack (the CPU's scratch)
registers — the VM's running state
stack list · bottom → top
what's happening
beat 1 / 1

06Two districts of memory: stack and heap

The RAM your program is given doesn't stay one undivided block — but before naming the two pieces it splits into, it helps to feel the actual problem that forces the split. A running function needs a small scrap of private space: room for its arguments, its local names, and one thing you'd never think to ask for — a note of where to jump back to once it's finished. Now imagine one function calling another, which calls a third. Each of those calls needs its own locals and its own return note. And here's the detail that decides everything: nested calls always finish in the exact reverse order they started in — the last one called is always the first to hand control back. That's not a style choice; the nesting forces it. So this bookkeeping is created and destroyed in strict last-in, first-out order.

But a second fact cuts right across the first: some values need to outlive the call that created them. A function that builds a list and hands it back can't let that list disappear just because its own locals are cleared away — the caller is still holding onto it. So a running program has two kinds of lifetime that don't line up: bookkeeping, which disappears in perfect last-in-first-out order, and objects, which live for as long as something, anything, still points at them. One single block of memory can't serve both needs well — so the machine splits its memory into two separate regions, each one shaped to fit the lifetime it has to support. Once you can feel which region you're in, the language stops feeling mysterious.

The stack holds the bookkeeping, and because that bookkeeping is strictly last-in-first-out, it can be stored in the cheapest way possible: one continuous run of numbered boxes with a single marker showing the top. Quick reminder of a word we've been using: a function is a named mini-program living inside your program — something like play_song. You call it to run it, and it returns to whoever called it once it's finished (you'll write your own in chapter 9). Every call that's currently running gets a frame: a block holding its local names and its return address. A call pushes a new frame by moving the top up by the frame's size; a return pops it by moving the top back down — last opened, first closed, which is what people mean by LIFO. And this rigid shape pays off: there's no searching and no ledger of free space to maintain — allocating a frame is just "add to a pointer," and freeing one is "subtract from a pointer." That's the entire reason the stack is fast, and also why it's called a stack: it only ever grows and shrinks at one end, like a spring-loaded stack of plates. It even cleans up after itself for free — when a call returns, the top simply moves back down, and its frame is gone.

the stack: LIFO
Function-call bookkeeping. Fast, because allocating and freeing is just moving a pointer.
the heap: any order
Objects that must outlive their call. Freed by reference count, not by LIFO order.

The heap is shaped for the opposite kind of lifetime. Objects don't follow last-in-first-out order — a list created early can easily outlive one created much later — so the heap works more like an open warehouse floor, where a block of any size can be claimed at any address and released at any time, in any order. The number 200, the text "Blinding Lights", and every playlist and object you build later all live here. That flexibility comes at a price the stack never has to pay: the allocator has to keep track of which regions are free and search for one big enough whenever it's asked. It's also the reason for an extra step you'll rely on throughout this course — a name sitting in a stack frame doesn't hold the object itself, it holds the address of an object living over on the heap. And cleanup here isn't vague or automatic in some mysterious way: every heap object keeps a reference count — a running total of how many names point at it — and the moment that count reaches zero, the object is freed immediately. (A separate collector only steps in to catch cycles: objects that point at each other and keep each other's count above zero, even though nothing outside the group points in.)

THE ONE SENTENCE TO MEMORISE
Plant this deep — it is the single most useful sentence in the course: in Python, your values live on the heap, and your names just point at them. A name is never a box holding a value; it's an arrow aimed at an object out on the heap. Chapter 3 will prove it to you with a one-line experiment you can run yourself.

One honest note, because this course tries not to sell you a tidy lie: in CPython (the standard version of Python you'll install in the next section), frames are actually small objects that live in heap memory too. So "the stack" isn't really a separate, fenced-off area of hardware somewhere else — it's the name for the strict last-in-first-out discipline those frames follow. The roles shown in the picture below are exactly right; only the idea of two separate physical locations is a simplification, and now you know exactly where that simplification lives.

your program's RAM, split by jobfigure
your program's RAM frame: main program frame: play_song() grows ↓ on each call, shrinks on return 200 "Blinding Lights" "Titanium" STACK — call bookkeeping one frame per running function last in, first out; cleans itself tiny, rigid, predictable HEAP — where objects live any size, any order, any lifetime freed when its reference count hits 0
Fig 06 — Stack for who is doing what, heap for the things themselves. Every chapter ahead lives somewhere in this picture — keep it in your mind's eye.

Reading a static picture is one thing; watching frames actually move is another. Use the step control below to walk through a tiny run: main calls play_song("Blinding Lights"), that call finishes and returns, and finally the collector cleans up. Notice the two regions move on completely different schedules — the stack snaps back the instant a call returns, while heap objects stick around until nothing points at them anymore.

call stack vs. heap, step by stepinteractive
0 · program starts
STACK — running now
HEAP — the objects
Step 0: only main is running. Its frame sits alone on the stack; the heap is still empty.
Fig 06b — The stack empties itself the moment a call returns; a heap object holds on only until its reference count — the running tally of names pointing at it — drops to zero, at which instant it's freed on the spot.
Wait — if the heap stores the number 200 as a full-blown object, how big is that object? A single byte held 200 just fine back in the counting section (a byte tops out at 255) — does Python really spend more than that on such a small number?

It does, and the cost is real: a small integer object costs 28 bytes in CPython — you can check this yourself with sys.getsizeof(200) — and chapter 2 will open one up and show you exactly where those bytes go. That's roughly twenty-eight times more than the raw value needs. Why pay that cost? Because a Python integer isn't just the value 200 — it also carries its type, a reference count, and enough extra machinery to grow past a byte, past a billion, past anything your hardware can natively count, all without you ever having to think about it. That trade-off — space in exchange for flexibility — is a theme that runs through this entire course, and now you've seen it up close.

the frame is real too
In CPython, frames themselves are heap objects — "the stack" names a discipline, not a separate slab of hardware.
space for flexibility
One int costs 28 bytes to hold a value that needs one — the price of type + refcount + headroom.
The deeper cut: why the stack can afford to be so fast

Pushing and popping a frame is about as cheap as computing gets. Because the stack only ever changes at the top, allocating a frame is basically just moving one pointer down, and freeing it is moving that same pointer back up — no searching, no bookkeeping ledger, no collector needed. That's exactly why local names are fast to create and clean themselves up automatically.

The heap pays for its flexibility with exactly the work the stack gets to skip. Because objects can be any size and can die in any order, the heap ends up with gaps whenever things are freed, and something has to keep track of what's still alive versus what's garbage. CPython's main technique is reference counting: every object keeps a running count of how many names point at it, and the moment that count hits zero, the object is freed immediately. A separate cycle-detecting collector only steps in for the rarer case where two or more objects point at each other but nothing from outside points at any of them. You'll work with reference counts directly in chapter 3 — the interactive above is the gentlest possible preview.

Enough theory — time to make the ladder fire for real. Next: install Python, type one line, and watch print("Blinding Lights") travel from your keyboard, down through every rung, to the bare metal and back →

Trace3 step the machine — idea · code · memory move together
Two memory districts — the stack that snaps back, the heap that lingers
A running program splits its RAM in two: the stack keeps one frame per running call (last in, first out, self‑cleaning); the heap keeps the actual objects (any size, freed later by the collector). Press Next and watch a call push a frame, make objects, then return and let them die.
stack · LIFOheap · GC2 objects · 84 B
The two districts — stack (calls) · heap (objects)RAMrun
Stackcall frames · LIFO
main()
no locals
play_song(title)
title, length
Heapobjects · freed by GC
str'Blinding Lights'no refs56 B
int200no refs28 B
In plain words
Under the hood
RUNALU · the operation
Program · memory.py
in scope · type
Memory · stack + heap
registers — the running state
heap · objects what's allocated right now
what's happening — the two clocks
Stack
Heap
beat 1 / 1
PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 00, in working code

Every program here is a real Python 3 program about the same thing a computer is made of — bits. We start by writing numbers in other bases, wire up the four logic gates, slide bits sideways, then handle raw bytes and the characters they encode. Press Next and watch the bits move.

Counting past ten — bases & literals
The same integer can be written in binary, hex, or octal. Python's literals and inspector functions just change the costume, never the number.
The four logic gates — & | ^ ~
Bitwise operators work on every bit in parallel. AND masks, OR sets, XOR toggles, NOT flips — the whole of digital logic in four symbols.
Sliding bits — shifts & masks
Shifting moves every bit left or right, doubling or halving. Combine shifts with an AND mask and you can pack many fields into one number and pull them back out.
Bytes — the raw material
A bytes object is a sequence of numbers 0–255. Converting ints to and from bytes is how numbers cross wires and files — and why a byte that overflows quietly wraps.
Characters are just numbers
Text is bytes wearing a lookup table. ord gives a character's code point, chr turns a number back into a character — and once you see that, letters do arithmetic.
end of chapter 00 · six sections
iolinked.com
Written by Ajai Raj