python/volume-1/chapter-04ch 04 / 64 · 20 min

04Operators & expressions

In Chapter 3 we pinned names onto objects. In this one we make those objects do something. Here's the plan. Every scrap of code you write is an expression — a question Python answers by boiling it down to one value — and operators are the verbs that ask it. We'll add two song lengths and watch 200 + 245 collapse to 445. We'll split that total with /, //, and % to turn raw seconds into 7:25. We'll tell == apart from is, find the side of and Python never bothers to read, and flip raw bits with & | ^ ~ << >>. All the way through we keep asking the one thing that matters — when a line has more than one operator, who binds first? By the end you'll read any expression the exact way Python does, with precedence in hand and no magic left in it.

An expression is a question; operators are the verbs One slash, two slashes, and the leftover Two kinds of equal: == and is Truthiness, and the logic that stops early
scroll to begin

01An expression is a question; operators are the verbs

Let's start with the smallest useful thing you can ask. You've got two song lengths — 200 and 245 — and you want the total. Type 200 + 245, press Enter, and Python does exactly one thing. It answers. It keeps working until a single value is left standing — here, 445. That scrap of code that boils down to one value has a name: we'll call it an expression. Every expression shares that one job: collapse to a single answer. 200 + 245 is an expression. So is total // 60. So is a bare 445, because a value sitting alone is the smallest expression there is. The symbol doing the work is the operator — the verb of the sentence — and the values it grips are its operands. So 200 + 245 isn't really arithmetic homework. It's a tiny question — how long are these two songs, back to back? — and + is the verb that answers it.

THE ONE IDEA TO CARRY FORWARD
Every expression, however sprawling, collapses to exactly one value. 2 is already an expression; so is 200 + 245; so is a page-long formula. "Running" an expression means only this: keep applying operators to operands until a single value survives. That survivor is the answer.
Anatomy of one expressionfigure
the question: “how long are these two songs, back to back?” 200 + 245 445 operand operator · the verb operand the value one expression
Fig 1 — Anatomy of an expression: two operands, one operator, a single value as the answer. Every expression in Python, no matter how tangled, has this shape once you zoom in.

Python ships seven arithmetic operators — + - * / // % ** — and most are familiar symbols in a slightly different form. Multiplication's × is typed * because no keyboard has a times sign. Division's ÷ becomes / for the same reason. ** is the power operator: 7 ** 3 means 7·7·7 = 343, not 7·3. That leaves two genuine strangers, // and % — both are kinds of division, and they're odd enough to get the whole next section to themselves. Point every one of the seven at the same pair of numbers and you get seven different answers. Drag the second number below and watch them diverge.

One pair, seven verbsinteractive
one pair — 7 and b — asked seven different ways + - * / // % ** 7 + 3 7 - 3 7 * 3 7 / 3 7 // 3 7 % 3 7 ** 3 10 4 21 2.333… 2 1 343 with two whole numbers, only / is forced to a decimal — every other result type follows the operands
3
Drag b from 1 to 9. Watch // and % shift as b grows past 7 — once the divisor is bigger than 7, the whole-part drops to 0 and the leftover becomes all of 7 — and notice / is the only card that ever shows a decimal point.
Fig 2 — One pair, seven verbs. Slide b and every card re-answers at once — proof that an operator is just a question waiting on its operands. Inside this widget's regime — a = 7 and a positive whole b — only / ever shows a decimal. But that is not a law about the operators: the result type is decided by the operands. Feed any card a float and its answer floats too (7 % 2.0 is 1.0), and even ** floats on a negative power (7 ** -1 is 0.142…). That lonely dot under / is the crack the next section pries open.
Wait — when I write total = blinding + titanium, does + change blinding or titanium? Neither. An operator reads its operands and hands back a brand-new third value; the two originals sit there untouched, ready to be asked again. A verb like + is a question about its operands, never a command to them.

Now watch Python answer a stack of these questions in a row. Each print below hands a finished expression to the screen. Read the comment on each line as the answer Python works out.

songs.pypython
# our playlist, in seconds — chapter 0's two anchors
blinding = 200                 # "Blinding Lights" — The Weeknd
titanium = 245                 # "Titanium" — David Guetta ft. Sia
total = blinding + titanium    # one + verb, two operands
print(total)                   # 445  — the whole playlist, once
print(total * 2)               # 890  — played twice, end to end
print(7 ** 3)                 # 343  — ** is power: 7·7·7, not 7·3

plays = 0
plays += 1                     # augmented assignment: read, add, rebind
print(plays)                   # 1

That last line hides the section's one real subtlety. += is augmented assignment, and for a number it means exactly three things: read plays, add 1, rebind plays to the result. Rebind is the exact word. An int is a photo, not a whiteboard (chapter 3), so += can never edit the 0 object itself. Python computes 0 + 1 and slides the name onto the answer. Here the answer isn't even freshly built: it's the 1 already waiting on chapter 3's small-integer shelf. Past the shelf — imagine 256 ticking to 257 — the result would be built anew. Either way plays lands on a different object than it started on: new value, new id(). That is the whole truth for numbers, and hold onto it, because it is about to matter. Point two names at one list and += does the opposite: it edits that very object in place and id() never moves, so a change made through one name quietly shows up under the other. That one difference is the seed of a whole class of aliasing bugs you'll hunt in chapter 6. The deeper cut below shows the split.

What plays += 1 actually movesfigure
plays += 1 — same name, different object, different id() plays before now · += 1 0 id ≈ 0x…9e0 1 id ≈ 0x…a10 both numbers live on chapter 3's small-integer shelf — pre-built, shared, and frozen
Fig 3 — plays += 1 never touches the object 0; it moves the name plays onto a different object (the shelf's 1), so id(plays) changes. The two ids shown are illustrative — run it yourself and they will differ, but they will differ.

✗ The myth

plays += 1 is faster than plays = plays + 1 because it bumps the number in place instead of building a new one.

✓ The reality

For an int there is nothing to bump — the object 0 is frozen (chapter 3). += computes 0 + 1 and re-aims the name at a different object; here it even lands on the pre-built 1 from the small-integer shelf. Same work, same result — += is simply fewer keystrokes.

↺ reframe
+= is not "add into." Read it as three moves compressed into one keystroke: read the name's current value, add, then rebind the name onto the answer. The number never moves — the name does.
EXPRESSIONS ANSWER, STATEMENTS ACT
blinding + titanium is an expression — it produces 445. But plays = 0 is a statement: it does something (binds a name) yet hands back no value, which is why x = (plays = 0) is a syntax error, not a clever trick. The lone exception is the walrus :=: it lets an assignment also hand back the value it just stored. That earns its keep in exactly one situation — when you need a value and want to remember it in the same breath, so you never compute it twice. You have not hit that situation yet; it arrives with loops and comprehensions in chapter 5, where while (chunk := read()): both tests the chunk and keeps it. File it away — (n := 5) + 1 is 6 and n is now 5 — and reach for it there, not here.
The deeper cut — is += always just x = x + …?

For frozen values — int, float, str, tuple — yes, exactly: read, add, rebind, land on a new object. But calling that "the rule" is a labelled simplification, so here is the refinement. When the left side is a mutable object like a list, += is allowed to change that very object in place, and it does. Watch the identities:

inplace.pypython
L = [200, 245]
before = id(L)
L += [230]         # grows the SAME list — before == id(L)
M = [200, 245]
M = M + [230]      # builds a NEW list — id(M) changes

So L += [230] and L = L + [230] are not the same statement. The first keeps the list's identity; the second replaces it. Under the hood, += first asks the object "can you extend yourself?" — it looks for a method named __iadd__ (a method is just a function that rides on the object's type, the machinery you'll build in chapter 12). A list has one and mutates in place. An int does not, so Python quietly falls back to plain x = x + 1. We meet mutation properly in chapter 6. For numbers, "read, add, rebind" is the whole truth.

Two of those seven verbs — // and % — are still just "divide," yet Python keeps them apart from / on purpose. One slash, two slashes, and the leftover: three ways to split a number, each answering a different question. →

Wait — put blinding + titanium on a line all by itself and Python still does the work — it computes 445 and then throws it straight in the bin, because no name reached out to catch it. That's a bare expression statement: the verb runs, the answer evaporates. The only reason print(…) seems different is that printing is a side-effect on the way out, not the value handed back to you.
THE OPERATOR THAT ENDED AN ERA
The walrus := from this section is the most divisive syntax Python ever shipped. The battle over its proposal (PEP 572) was so bruising that days after it was accepted, in July 2018, Guido van Rossum — Python's creator — stepped down as "Benevolent Dictator For Life," the final-say role he had held for nearly three decades, saying he was tired of the fight. One three-character operator helped end the buck-stops-here era of the language.
ONE VERB, MANY MEANINGS
+ is not hard-wired to numbers. 'Blind' + 'ing' glues strings into 'Blinding'; [200] + [245] joins lists into [200, 245]. The operator simply asks its left operand, "do you know how to add?" — and each type answers in its own way. It's also why 200 + '245' raises TypeError: an int and a str can never agree on what + should mean, so Python refuses to guess.
Trace3 step the machine — idea · code · memory move together
What plays += 1 actually moves
For a number, += is not “add in place.” It is three moves — read the value, add, then rebind the name onto a different object. The number 0 is never touched; only what the name points at changes, so id() changes.
int · immutable+= · read·add·rebindid() changes
The name-tag and the small-integer shelfint · frozenbind
plays
small-integer shelf · pre-built, frozen objects
int
0
int
1
READ
ADD
REBIND
In plain words
Under the hood
BINDALU · the operation
Program · rebind.py
variables · type
Memory · registers + shelf
registers — the running state
small-int shelf frozen objects · which one plays is on
what's happening
same name → different object → different id()
beat 1 / 1

02One slash, two slashes, and the leftover

"Blinding Lights" runs 200 seconds. Your music app shows 3:20. Those are the same fact in two forms, and getting from one to the other is exactly the gap between Python's three ways to divide. Learn these three symbols and you can turn any raw count into minutes-and-seconds, pages, rows, change owed, or the hour on a clock. They look nearly identical. They answer three genuinely different questions.

A single slash, /, is true division. It answers "exactly how many times?" and it has one unbreakable habit: it always hands back a float, a number carrying a decimal point. Not sometimes. Not only when it doesn't divide evenly. Even 6 / 3 comes back as 2.0, never a bare 2. One slash, always a float. Remember that, because it surprises everyone exactly once.

Two slashes, //, is floor division. It answers "how many whole times?" — do the division, then round down to a whole number. 200 // 60 is 3: three complete minutes have gone by, and it drops the sliver left over.

The percent sign, %, is the remainder (say it aloud as "mod"). It answers "what's left once the whole times are taken out?" 200 % 60 is 20: twenty spare seconds that couldn't fill another whole minute.

// and % are a matched pair — two halves of one act. One counts the full blocks; the other keeps the leftover. Together they account for every second and lose nothing: three minutes of sixty, plus twenty over, is two hundred again. That completeness isn't luck. In a moment you'll see Python guarantee it.

THE ONE IDEA TO CARRY FORWARD
One division, asked three ways. / wants the exact ratio (and always answers in floats). // wants the count of whole blocks. % wants the leftover. The last two are inseparable: // and % together lose nothing.
200 seconds, split three waysfigure
"Blinding Lights" = 200 s 3:20 60 s 60 s 60 s 20 s 3 blocks × 60 = 180 s + 20 s 200 // 60 = 3 the FULL blocks · whole minutes 200 % 60 = 20 the LEFTOVER · spare seconds 200 / 60 = 3.3333… the EXACT ratio · always a float
Fig 1 — Three symbols, three questions. // counts whole blocks, % keeps the leftover, / gives the exact ratio. The green blocks plus the gold sliver are the whole 200 s — between them, // and % lose nothing.

Watch Python compute all three. Nothing here is magic. Each line asks one of those three questions and prints the answer:

divisions.pypython
total = 200                     # "Blinding Lights", in seconds
print(total / 60)               # 3.3333333333333335  — exact ratio, a float
print(6 / 3)                    # 2.0  — still a float, even when it divides clean
print(total // 60)              # 3   — whole minutes only
print(total % 60)               # 20  — the seconds left over
print(divmod(total, 60))        # (3, 20)  — both answers in one call
print(f"{total // 60}:{total % 60:02d}")   # 3:20
print(f"{245 // 60}:{245 % 60:02d}")       # 4:05  — :02d pads the 5 into 05

Here's how those last two lines work. An f"…" string fills each {…} hole with the value of the expression inside it, so {total // 60} becomes 3. The :02d added to the second hole is a formatting instruction: "print this as a decimal integer, at least 2 digits wide, padding with a 0." That's what turns a lone 5 into 05, so "Titanium" reads as a proper 4:05 and not a broken 4:5.

TWO ANSWERS, ONE CALL
// and % almost always travel together, so Python ships them as a combo: divmod(200, 60) returns the pair (3, 20) — quotient and remainder computed in a single step. The whole part and the leftover, handed back at once.
Drag a song's length — watch // and % carve it upinteractive
200 // 60 = 3 full minutes 200 % 60 = 20 s left over 3:20 each green block = 60 s (one full minute) · amber bar = the % remainder
200 s → 3:20
Try 245 ("Titanium") — it lands on 4:05. Push it to 600 and the remainder vanishes: a clean 10:00. The green blocks are //; the amber sliver is %; drop either and seconds go missing.
Fig 2 — // counts the whole green minutes, % is the amber remainder, and the two always re-add to the number you dialed in. That's the completeness the last section promised.
% IS THE "WRAP-AROUND" OPERATOR
Anything that cycles is a job for %. hour % 12 keeps a clock in range; n % 2 is 0 for even numbers and 1 for odd; i % len(playlist) loops a track index back to the top after the last song. Whenever a value should roll over at some limit, reach for the remainder.
Wait — if 6 / 3 is 2.0, is there any pair of numbers where a single slash gives back a plain integer? No — not one. / is defined to produce a float every single time, so the type of the answer never depends on the numbers you feed it. That predictability is the whole point: the code downstream always knows exactly what shape it's getting, and never has to guess.

So far, so tidy. But watch what "round down" really means the instant a number turns negative, because this is where half of all programmers slip. "Round down" does not mean "chop the decimals off." It means round toward negative infinity — always leftward on the number line, toward the smaller value. Slide the dividend below zero and watch // step down to the next multiple, never up toward zero.

Slide a below zero — see // floor toward −∞interactive
a ÷ 3 · the green ring is the multiple of 3 at or below a; the gold gap up to a is a % 3 −9 −6 −3 0 3 6 9 a −7 // 3 = −3 −7 % 3 = 2 (the gap up to a · never negative) 3 × (−3) + 2 = −7 ✓
a = −7
The green ring always snaps to the multiple of 3 at or below a — leftward, toward −∞. Truncation would jump the other way (to −6 for −7); Python never does. The gold gap is a % 3, and for a positive divisor it stays 0, 1, or 2 — never negative.
Fig 3 — // rounds down the line, not toward zero — so -7 // 3 is -3, and the remainder -7 % 3 is the non-negative gold gap of 2. The two always rebuild a.
↻ reframe
// and % were never two operations — they're one operation glimpsed through two windows. There is a single act, "divide and keep the remainder," that yields a quotient and a leftover in the same breath; divmod(a, b) is that act named directly. Writing a // b and a % b on separate lines just performs the one act twice, throwing away half the answer each time.

✗ The myth

a // b is just a / b with the digits after the decimal point chopped off.

✓ The reality

True for positives, wrong for negatives. Chopping -2.33 toward zero gives -2; flooring it gives -3. Python floors — -7 // 3 is -3, not -2. Same symbol, a different rule than C or JavaScript follow.

The deeper cut — the contract that ties all three together

Why does Python floor toward negative infinity instead of truncating toward zero the way C and JavaScript do? To keep one promise it makes for every pair of numbers, the invariant the figures above were quietly obeying:

a == b * (a // b) + a % b  —  rearranged, a % b == a - b * (a // b).

Because // floors, the remainder is forced to carry the divisor's sign and to sit in the half-open range [0, b) for a positive b. So -7 % 3 is 2 (not -1), and flipping the divisor's sign, 7 % -3 is -2. C truncates instead, so its -7 % 3 is -1. Identical symbols, a different contract underneath. Worth knowing before you port a formula between languages and quietly get the wrong sign.

A second, sharper edge: on huge whole numbers, // stays exact while / can quietly lie. A single slash must route its answer through a float, and a float only has room for about 16 significant digits. So (10**18 + 7) // 1 is exactly 1000000000000000007, but (10**18 + 7) / 1 rounds off to 1e+18 — the last digits simply vanish. When integers get big and every digit matters, reach for //, never /.

And a reassurance: // and % aren't integer-only. Hand them a float and they cooperate — 5.5 // 2 is 2.0 and 5.5 % 2 is 1.5. The result is a float whenever either operand is a float, and the floor-and-remainder logic is exactly the same.

THE ONE THING THAT CRASHES
All three choke on a zero divisor. 1 / 0, 1 // 0, and 1 % 0 each raise ZeroDivisionError and halt your program on the spot — no "infinity," no silent 0 slipped in behind your back. If a divisor could ever be zero (an empty playlist, a count that hasn't loaded yet), you guard it before you divide.

Numbers answered with numbers. The next family of operators answers every question with just one of two words — and forces you to meet two subtly different notions of "equal": == and is. →

% HAS A SECRET SECOND JOB
On numbers, a % b is the remainder. On a string, the very same % is the old formatting operator: '%d:%02d' % (7, 25) builds '7:25', padding the 25 for you — the identical job an f-string does. One symbol, two unrelated powers, chosen entirely by the type sitting on its left.
Wait — exactly when does a single slash start lying? At 2**539,007,199,254,740,992. Past that point a float runs out of room to tell consecutive whole numbers apart, so (2**53 + 1) / 1 collapses back to 2**53, the + 1 simply gone. // never blinks: (2**53 + 1) // 1 stays exact forever. When integers get big and every digit counts, / is the one you can't trust.
// FLOORS — IT DOESN'T CHOP
// isn't integer-only and it isn't "delete the decimals." It always steps left on the number line. 5.5 // 2 is 2.0 (yes — a float), and -5.5 // 2 is -3.0, not -2.0. Truncation would round toward zero; flooring rounds toward −∞. On positives you'd never spot the difference — which is exactly what makes the negative case a trapdoor.
Wait — because Python floors toward −∞, a remainder with a positive divisor is never negative — even for negative inputs. -1 % 12 is 11, not -1: the clock wrapping backwards from 12 round to 11. That's why tracks[-1 % len(tracks)] lands cleanly on the last song instead of crashing. In C the same line gives -1 and the index walks off the edge; Python's flooring makes wrap-around just work.
Trace3 step the machine — idea · code · memory move together
Carving 445 seconds into 7:25
Floor division and remainder are one act seen twice: the count of whole 60-second blocks is //; the final leftover is %. The strip below peels off one block at a time — but read that as a picture of the answer, not the mechanism. The CPU does not loop and subtract 60 over and over: a single divide produces the quotient and the remainder together (exactly what divmod(445, 60) hands back as (7, 25)), in one step no matter how many blocks there are. Press Next and watch 445 resolve to 7:25.
blocks of 60// & %445 → 7:25
The idea — carve 60-second blocks out of 445445s → 7:25init
In plain words
Under the hood
SETALU · the operation
Program · carve_time.py
variables · type
Memory · registers + object
registers — the running carve
clock str · the m:ss timestamp
what's happening
beat 1 / 1

03Two kinds of equal: == and is

The comparison operators — == != < > <= >= — are a panel of yes/no judges. Each one asks a single question and always answers with a bool: True or False, never anything in the middle. Feed the panel 200 < 245 and it returns True. Feed it 245 == 245 and it returns True. There is no "sort of" and no "close enough." The verdict is one bit, the smallest answer a machine can give.

And Python is a strict judge: 7 == "7" is False. It will not quietly turn the string "7" into the number 7 to force a match. An int and a str are simply different kinds of thing, so the honest answer is "no, not equal." For the same reason 1 != "1" is True. Other languages fudge this; Python refuses to, and that refusal will save you from a whole class of silent bug.

What about comparing two strings — is "Avicii" "less than" "Daft Punk"? Yes, and it's decided the way a dictionary is ordered: character by character, by code point, the plain number behind each letter. ord('A') is 65 and ord('D') is 68. Python lines the two words up and looks at the very first pair — A versus D, 65 versus 68. The moment they differ, the match is settled. 65 < 68, so "Avicii" < "Daft Punk" is True, and not one of the remaining letters is ever read. By the same rule "Apple" > "Orange" is False, because the first characters, A (65) and O (79), decide it on the spot.

One code point settles the whole wordfigure
"Avicii" A v i c ord 65 "Daft Punk" D a f t ord 68 65 < 68 → decided here the rest of the letters are never even read → True
Fig 1 — String order is dictionary order, resolved one code point at a time. The first pair that differs delivers the whole verdict; everything after it is dead weight the comparison never touches.

So far, so intuitive: == is asking "do these two things carry the same value?" But Python has a second, sharper question that looks almost the same and means something entirely different: is. And is is not a fancier ==. Where == asks "same value?", is asks "same object?" — literally the same thing in memory, the same identity id() handed you back in chapter 3. Two things can carry identical values yet be two separate objects. That gap is the whole lesson.

equal.pypython
# three playlists, timed in seconds
a = [200, 245, 187]     # one list, built here
b = [200, 245, 187]     # a second list, same contents
c = a                   # NOT a new list — a second name for the first

print(a == b)           # True  — identical contents
print(a is b)           # False — two objects, two addresses
print(a is c)           # True  — one object wearing two labels
Same value, different objectsfigure
a c b id 0x1f…a10 [200, 245, 187] id 0x1f…b58 [200, 245, 187] a == b → True a is b → False a is c → True
Fig 2 — == walks into each box and reads the contents; is only checks which box you're pointing at. b matches a in value but lives at its own address; c is just a second label stuck on a's object.

Here is where is gets dangerous, and where an interactive earns its keep. For small, common integers, CPython quietly keeps a single shared copy — one pre-built 200 that every 200 in your program points at (chapter 3's singleton trick). So two independently-computed 200s can turn out to be the same objectis says True — by pure caching accident. Push the number past the cached zone and the accident disappears: now they're two objects and is says False, even though == never wavered. Slide a song's length across the boundary and watch is flip.

Same number — one object, or two?interactive
x y id 0x7f…e8 256 s id 0x7f…e8 256 s one shared cached object x == y → True x is y → True
256 s
Two names, both set to the same length, computed independently. Slide up: at 200 ("Blinding Lights") and 245 ("Titanium") they share one cached object, so is answers True — by luck. Cross 256 and they split into two objects; is flips to False while == never budges.
Fig 3 — == reads the value and says True the whole way. is only reflects an accident of where CPython stored the number — which is exactly why you must never trust it on numbers. (Addresses are illustrative.)
⚠️
Never use is on numbers or strings
CPython pre-builds the integers −5 to 256 as shared singletons, so x is 200 can come back True by caching accident — and False for 1000. It's an implementation detail, not a language promise. Modern Python even warns you: write x is 200 and it raises SyntaxWarning: "is" with 'int' literal. Did you mean "=="?. Reserve is for one honest job — comparing against a genuine singleton: x is None.
🔗
Chained comparison reads like maths
Python lets you write 180 <= seconds <= 240 to ask "is this song between three and four minutes?" It means 180 <= seconds and seconds <= 240, and — crucially — seconds is evaluated exactly once. Most languages can't do this; Python reads a range the way you'd write it on paper.
Wait — if == checks value, why is 0.1 + 0.2 == 0.3 False? Because floats are base-2 approximations (chapter 2's bytes striking again): the sum lands at 0.30000000000000004, a hair off 0.3. == is being scrupulously honest — the two values genuinely differ. It was the inputs that were never exact.
↺ reframe
Stop reading == and is as "equal" and "really equal." They answer different questions. == asks about the contents — do these carry the same value? is asks about identity — are these the very same object, one thing with two names? Two twins have == faces and are is-different people. Confusing the two isn't a small slip; it's asking the wrong question and mistaking the answer for the one you wanted.

✗ The myth

is is just a faster, stricter == — a performance trick for when you're sure the values match.

✓ The reality

They compute different facts. is is a one-instruction address check that ignores contents entirely; == reads and compares the values. is being "faster" is exactly why it's dangerous: it answers instantly and answers the wrong question.

The deeper cut — what == actually does, and why 7 == "7" is False

Earlier I said an int and a str are "different kinds of thing," so they're not equal. That's true enough to trust, but here's the real machinery. == isn't a hard-wired byte compare; it's a negotiation. Python asks the left object first: (7).__eq__("7"). The int looks at the str, doesn't know how to compare itself to one, and returns a special signal, NotImplemented — "not my department." Python then asks the right object: ("7").__eq__(7), and the str also returns NotImplemented. When both decline, Python falls back to identity: are they literally the same object? They aren't, so the final answer is False. Nothing was ever coerced; two objects just failed to agree they were equal.

This is also why == is something you can teach your own types. Define __eq__ on a Song class and song_a == song_b runs your rule for sameness. is can't be redefined; it always means "same object," full stop.

One last honesty note on is: strings get a caching trick too, called interning. Two identical string literals in your source often become one shared object, so "Titanium" is "Titanium" may be True. But build the same text at runtime and it's a fresh object, so is turns False. Same accident, same lesson: for value, always use ==; save is for None.

== forces every value into a clean True or False. But Python will happily judge values that are neither — and the instant it knows the verdict, it stops reading. →

Wait — is anything not equal to itself? Yes — exactly one thing. float('nan') ("not a number") is defined so that nan == nan is False: a value that fails its own equality test. And it's why nan is nan is still Trueis checks the object, not the value, so the two questions split clean apart right here.
A BOOLEAN IS SECRETLY A NUMBER
True == 1 and False == 0 are both True — because bool is a subclass of int. So True + True + True is 3, ['off', 'on'][True] indexes to 'on', and sum([True, False, True]) counts the Trues as 2. A yes/no is a 1/0 wearing a mask — which is why "how many passed?" is often just summing a list of comparisons.
Wait — 'Zebra' < 'apple' is True?! Yes — comparison reads code points, and every uppercase letter has a smaller one than every lowercase letter: ord('Z') is 90, ord('a') is 97. So a naïve sort files all Capitalized words ahead of lowercase ones — "Zebra" before "apple." It isn't a bug; it's the exact code-point rule from this section, followed without mercy.
== ALWAYS ANSWERS; < CAN REFUSE
7 == '7' quietly returns False — different types, not equal, no drama. But 7 < '7' raises TypeError. Equality is total: any two objects can be asked, and worst case the answer is "no." Ordering is not: Python won't invent a rule for whether an int is "less than" a str, so it stops you rather than guess.
Trace3 step the machine — idea · code · memory move together
How Python decides 'Titanium' < 'Titans'
Strings sort like a dictionary: compare one code point at a time, left to right (here 'i' is 105, 's' is 115, so 'i' comes first). The first column that differs settles the whole verdict — every later letter is never read. And if the columns never differ but one word runs out first, the shorter string wins: 'Titan' < 'Titans', because a missing letter counts as less than any real one. Press Next to watch the scan.
lexicographicdecides at i = 5left → right
The idea — two words, code point by code pointstr · code pointsinit
 
a
b
In plain words
Under the hood
SETALU · the operation
Program · strcmp.py
variables · type
Memory · registers + verdict
registers — the running state
verdict bool · a < b — the settled result
first differing code point wins — the rest is dead weight
what's happening
beat 1 / 1

04Truthiness, and the logic that stops early

Type if playlist: and Python does something quietly generous: it does not demand a True or a False. It hands the question to the list itself and lets it answer. Every value in Python can stand in for a yes-or-no, and bool(x) shows the verdict it would give. Why would a language bother judging every value this way? Because the real questions you ask rarely arrive as tidy booleans. They arrive as "is the queue empty?" or "did the user actually type a name?" Python lets the queue and the string answer for themselves, so you never have to write len(name) > 0 when if name: says the same thing more plainly.

The rule that decides it fits on one line: empty or zero is falsy; everything else is truthy. That is the whole law. The complete falsy lineup is short enough to memorise — False, None, 0, 0.0, "", [], {}, set() — eight values, each one either a kind of nothing or a kind of zero. Every non-empty string, every non-zero number, and every collection with even a single thing inside is truthy.

THE ONE IDEA TO CARRY FORWARD
A value's truthiness is not about its meaning — it's about whether it is empty or zero. Python never reads what a string says or what a number represents; it only asks "is there anything here?" Get that one distinction right and every surprise in this section dissolves.
The falsy eight — everything else is truefigure
FALSY — the empty and the zero (all eight) False None 0 0.0 "" [] {} set() TRUTHY — everything else (a few of infinitely many) 42 -1 "0" " " [0] "False" "0" and "False" are non-empty strings, so both are truthy. The only falsy string is the empty one, "".
Fig 04a — The falsy eight. If a value is not one of these — not empty, not a zero — Python treats it as true. Notice the trap on the bottom row: "0", " ", and "False" all hold something, so all three are truthy.

Three operators combine these verdicts. not flips one: not x is True exactly when x is falsy. a and b is truthy only when both are; a or b when at least one is. But the headline of this section isn't what they compute — it's how. and and or are lazy. Python reads them strictly left to right and stops the instant the answer is settled. This is short-circuit evaluation. In False and anything, the anything is never touched, because a single falsy operand already doomed the and. In True or anything, same story: one truthy operand already saved the or.

Wait — when the right side is "never touched," how thoroughly is it skipped? If that side were a function call that prints a line, or writes to a database, would the print still happen? No — genuinely, nothing on the skipped side runs. Python doesn't quietly evaluate it and hide the result; it jumps clean over the code. No computation, no memory, no side effect. The skipped half might as well not be there.

That "jump clean over it" is not trivia — it's a guard you'll reach for constantly. Put a cheap, safe test on the left and let it decide whether the expensive or dangerous test on the right ever runs:

guard.pypython
# the two canon tracks: "Blinding Lights" 200 s + "Titanium" 245 s
total, count = 445, 2
print(count != 0 and total / count > 180)   # True  — the average is 222.5 s

count = 0                                # now the playlist is empty
print(count != 0 and total / count > 180)   # False — total / count NEVER runs

When count is 0, the left side count != 0 is False, so the whole and is already decided and total / count — the division-by-zero that would crash your program — is never reached. The dangerous half sits right there in the source and simply doesn't run. Reverse the two halves and the identical program crashes with ZeroDivisionError. Order is the whole design. Drag the slider below and watch the left operand act as a gate for the right one.

The guard, live — drag the count and watch the right half switch on and offinteractive
count != 0 and total / count > 180 (total = 445 s) count = 2 count != 0 True and ✗ skips → the risky half 445 / 2 > 180 222.5 → True result: True both halves ran, both were truthy
2
Start at count = 2 (both canon songs). Slide to 0: the left goes False, the right greys out — the division never happens, no crash. Slide up to 3: the gate is open, the division runs, but now the average has dropped below 180 s, so the right half votes False on its own. Same result word, very different reason.
Fig 04b — and tests left first. A False on the left settles everything and the right operand is skipped whole (count = 0). A True on the left opens the gate and lets the right operand cast its own vote.
↺ the thing people get backwards
Most people are sure bool("0") and bool("False") return False — they read the string and believe it. They're both True. Truthiness never reads the meaning of a string, only whether it is empty. "0" is one character long, "False" is five; both hold something, so both are truthy. The lone falsy string is "". If you ever parse text and check if answer: to mean "did they say yes," you've just accepted "no" as a yes — because "no" is truthy too.
The rule is bigger than eight values
Those eight are just the everyday faces of a general law. Any zero of any numeric type is falsy — 0j, Decimal(0), Fraction(0) all vote False — and any empty container is falsy, including (), b"", and range(0). Even your own classes get a say: define __bool__ (or fall back to __len__) and an empty Playlist([]) can report itself falsy so if playlist: just works.

✗ The myth

Short-circuiting is a behind-the-scenes speed trick — Python still evaluates both sides, it just returns the answer faster. Harmless either way.

✓ The reality

The skipped operand is never executed at all. If it would have printed, called an API, or crashed on a divide-by-zero, none of that happens. Short-circuiting isn't an optimisation you can ignore — it's a control-flow tool you write on purpose, ordering cheap-and-safe before expensive-and-risky.

The deeper cut — and and or don't actually hand you a bool

Here's the refinement to the tidy story above. Strictly, and and or do not return True/False — they return one of their operands, exactly as it was, unconverted. a or b gives a if a is truthy, otherwise b. a and b gives a if a is falsy, otherwise b. With plain boolean operands the result happens to be a bool, which is why you never noticed. 3 and 4 quietly returns the integer 4, and type(3 and 4) is int, not bool. This powers a classic idiom for a default value: name = title or "Untitled". If title is the empty string "" (falsy), you get "Untitled"; if title is "Blinding Lights", you get that title back untouched. Only not always returns a genuine bool: not "" is exactly True, type and all.

and, or and not weigh a value as a single whole — one verdict for the entire thing. But chapter 0 promised operators that reach inside a number and grip its switches one at a time. Down to the bits, with & | ^ ~ << >>

FALSY IS NOT THE SAME AS EQUAL-TO-FALSE
[] is falsy, so if not []: runs — yet [] == False is False. No contradiction: bool([]) asks "is there anything here?" (no → falsy), while == False asks "does this empty list equal the boolean False?" (no → different objects). Truthiness and equality are two different questions; only one of them reads emptiness.
Wait — which zeros are falsy, and is "not a number" one of them? 0.0, -0.0, and even the complex 0j are all falsy — any zero of any numeric type. But float('nan') is truthy: it isn't zero and it isn't empty, so if float('nan'): fires. Truthiness never asks what a value means — only whether it's a zero or an empty, and nan is neither.
THE or-DEFAULT THAT EATS A REAL ZERO
The tidy idiom count = user_count or 10 quietly replaces a genuine 0 with 10 — because 0 is falsy, and or hands back its right operand whenever the left is falsy. If 0 is a valid answer (zero plays, zero unread), that's a silent bug. The fix is to test for the thing you actually mean: user_count if user_count is not None else 10.
Trace3 step the machine — idea · code · memory move together
The and that never runs the division
and reads left-first and stops the instant it is settled. A falsy left operand means the right operand is skipped whole — so a cheap test on the left can guard a crash on the right.
left-firstshort-circuitguards ÷ 0
The idea — a gate that shuts before the dividebool · and-guardinit
safe = count != 0 and total / count > 180
left operand
count != 0
?
and
OPEN
right operand
total / count > 180
?
In plain words
Under the hood
SETALU · the operation
Program · guard.py
variables · type
Memory · registers + object
registers — the running state
safe bool · the guarded verdict
what's happening
beat 1 / 1

05Down to the bits: & | ^ ~ << >>

Chapter 0 taught you the secret every int is hiding: it isn't really a number, it's a row of switches. 12 is the pattern 1100; 10 is 1010. Every operator you've met so far — +, *, == — treats that row as a quantity and does arithmetic on the whole thing. Six new operators work differently. They reach straight past the number and grab the switches themselves, one column at a time. Here they are: & (AND), | (OR), ^ (XOR), ~ (NOT), and the two shifts << and >> that slide the whole row sideways.

The first three work by laying one number on top of the other and reading down each column. Stack 12 over 10 and ask a yes/no question of each pair of bits. & keeps a 1 only where both switches are on → 1000, which is 8. | keeps a 1 where at least one is on → 1110, which is 14. ^ — "exclusive or" — keeps a 1 only where exactly one is on, the two switches disagreeing → 0110, which is 6. No carrying, no borrowing, no place value spilling into the next column. Just eight small votes, tallied straight down.

Reading 12 & 10 straight down the columnsfigure
8 4 2 1 a = 12 1 1 0 0 & b = 10 1 0 1 0 a & b = 8 1 0 0 0 the one green column is the only place BOTH switches are on
Fig 05 — 12 & 10 = 8. Each output column is decided by its own two input switches and nothing else — the eights column is the only one where both are lit.

One diagram only shows one operator, so here's the same machine with a dial on it. Set two four-bit numbers and pick which question to ask each column. Watch the bottom row light up, and notice that changing a's 8-bit never disturbs a's 1-bit's answer. The columns are independent.

Two 4-bit numbers, one operator — mix the columns yourselfinteractive
8 4 2 1 a = 12 1 1 0 0 0b1100 & b = 10 1 0 1 0 0b1010 a & b = 8 1 0 0 0 0b1000
12
10
&
& needs both switches on, | needs at least one, ^ needs exactly one. Try a = 12, b = 10 under each; then set both sliders equal and watch ^ go to zero — a number XORed with itself is always 0.
Fig 05b — Every operator here is a rule applied independently to each column. Nothing a column does can change its neighbour's answer — which is exactly what makes the next reframe click.
↺ reframe — there is no carry
When you add 1 + 1 in binary you get 10: a carry ripples left into the next column, so column 0 reaches over and touches column 1. That rippling is the whole reason addition is "slow" and has to march right-to-left. Bitwise operators never ripple. Bit 0 of the answer is a function of bit 0 of the inputs and nothing else; all eight columns can be decided at the very same instant, in parallel, in a single tick of the clock. That independence isn't a detail — it's the entire reason these operators are the fastest tools in the box.
What bits are really for: a fistful of flags in one int
Give every song property its own switch: EXPLICIT, DOWNLOADED, LOVED = 1, 2, 4. Now pack a song's whole state into one small integer with |: state = EXPLICIT | LOVED is 5 (0b101) — explicit, not downloaded, loved. The full flag lifecycle is four moves, one operator each: set with state | LOVED; test with state & LOVED (→ 4, truthy — the flag is on; state & DOWNLOADED0, off); clear with state & ~LOVED (the ~ you just met turns LOVED into an all-ones mask with a single hole, so AND blanks exactly that bit and leaves the rest); and toggle with state ^ LOVED. Three independent yes/no facts riding in one number — the very trick behind Unix file permissions (chmod 755), network-packet flag fields, and graphics options, and the reason a whole config can travel as a single int.

The last two operators don't combine two numbers at all — they take one number and slide its whole row of switches sideways. x << 1 shoves every bit one place to the left and feeds a fresh 0 in on the right; x >> 1 shoves everything right and lets the low bit fall off the edge into oblivion. You already know this move in base ten: appending a 0 to 34 makes 340, ten times bigger; chopping the last digit makes 3. Same trick, base two — so a left shift doubles and a right shift halves. 5 << 2 is 5 × 4 = 20; 200 >> 1 is 100, the length of "Blinding Lights" cut in half. One catch worth its own breath: because the low bit simply drops off, >> always rounds down. 201 >> 1 is not 100.5 — it's 100, the same floor as 201 // 2.

Shift the row — every step left doubles, every step right halvesinteractive
256 128 64 32 16 8 4 2 1 value 0 0 0 0 0 0 1 0 1 result 0 0 0 1 0 1 0 0 0 5 << 2 = 20
5
<< 2
Push shift right (positive) to slide the switches left and multiply by 2 each step; pull it left (negative) to slide right and halve. Drag it to -3 on an odd value like 5 and watch bits tumble off the right edge — that's flooring, live.
Fig 05c — The blue row never changes; the green row is the same switches after the slide. Left is ×2 per step, right is ÷2 (rounded down) — the low bits that fall off the edge are simply gone.
Shift + mask = pull a field out of the middle
Shifts and & team up to extract a field — and shifts with | team up to build one. A colour like the green 0x5ede78 from chapter 0 is three bytes crammed into one int — red, green, blue, eight switches each. And 0xFF is nothing but those eight switches all on: 0xFF = 0b11111111 = 255. To read the red byte, slide it down to the bottom and keep only the low eight bits: (0x5ede78 >> 16) & 0xFF94. The >> 16 lines the red field up over the 1s column; the & 0xFF mask blanks every switch above bit 7, leaving red standing alone. To go the other way and pack three channels back into one number, shift each into its own lane and OR them together: (r << 16) | (g << 8) | b. Unpack with shift-and-mask, pack with shift-and-OR — the exact move a program makes every time it touches a pixel, a network header, or a date squeezed into one integer.

That leaves ~, the loner — it takes a single number and flips every switch, 10. Flip the bits of 12 = 1100 and you'd swear the answer is 0011 = 3. Run it and Python says ~12 is -13. What? The trick is a thing chapter 0 whispered and this operator shouts: a Python int doesn't stop at the four bits you can see. It carries an endless train of leading zeros off to the left — …0000 1100 — and ~ flips every last one of them into a 1. An endless run of leading ones is precisely how binary writes a negative number, and the result lands, without fail, on ~x == -x - 1.

Why ~12 is -13, not 3figure
~ flips EVERY switch — including the endless leading zeros you never see a = 12 0 0 0 0 1 1 0 0 ~ flip every switch ↓ ~a = -13 1 1 1 1 0 0 1 1 the gold ones are the trap: an endless run of leading 1s IS a negative number the 4 bits you saw did flip (1100 → 0011) · every whole number obeys ~x = -x - 1, so ~12 = -13
Fig 05d — "Flip the four bits I can see" is only half the story. Python flips the infinite leading zeros too; that sea of ones is what makes ~12 come out negative, exactly at -x-1.
Wait — if ~ really flips infinitely many bits into ones, why doesn't ~12 need infinite memory, and why isn't it a gigantic positive number? Because those endless ones aren't stored one by one — a leading run of all-ones is just the agreed-upon shorthand for "negative," so Python records it as the tidy little number -13 and reconstructs the ones on demand. The infinity is in the interpretation, not in the storage.
The deeper cut — "flip the bits you see" is a labelled lie; the truth is two's complement

When we said "flipping 1100 gives 0011 = 3," that was a deliberate simplification — true only if a number had a fixed, finite width with nothing to its left. Real Python integers don't. They use two's complement with infinite sign-extension: a non-negative number is padded with an endless run of 0s, a negative number with an endless run of 1s, and that leading digit is the sign. Flipping turns the zeros-sign into a ones-sign, so the answer must be negative. The exact value drops straight out of the arithmetic: two's complement defines a bit-pattern's value so that flipping all bits then adding one negates the number — -x == ~x + 1 — and rearranging gives the identity you can lean on forever, ~x == -x - 1. That's why ~0 is -1 (flip the endless zeros to endless ones — pure negative one), ~12 is -13, and ~200 is -201. No special case, no magic: just the honest, full-width picture the four-bit cartoon was standing in for.

Step back and notice what none of these six operators did: none of them built anything. No new container was allocated, no list copied, no object cloned. Each result is just another int, assembled from switches the operands were already holding. At the level of the metal, &, |, ^, and the shifts are each a single CPU instruction — one tick of the clock, no loop, no memory hunt. That's why, in the machine, bitwise work is about the cheapest thing a processor can do.

Fast at the metal — not automatically fast in your loop
"Single instruction" is a claim about the hardware, and it's true there. But in CPython, timing a & b against a + b shows them neck-and-neck (about 15 ns versus 13 ns each on this machine) — because the real cost is the interpreter unpacking two int objects, not the one-cycle operation buried inside. So reach for &, |, << because they say exactly what you mean about bits — packing flags, masking fields — not as a superstition that they'll magically speed up ordinary arithmetic.
bitwise.pypython
# six operators that reach past the number, down to its switches
print(12 & 10)      # 8   1100 AND 1010 = 1000  (keep where BOTH are 1)
print(12 | 10)      # 14  1100 OR  1010 = 1110  (keep where EITHER is 1)
print(12 ^ 10)      # 6   1100 XOR 1010 = 0110  (keep where EXACTLY one is 1)
print(5 << 2)       # 20  slide left 2  -> x 4
print(200 >> 1)     # 100 slide right 1 -> half of "Blinding Lights"
print(~12)          # -13 flip every bit: ~x == -x - 1

✗ The myth

| and & are just fancy spellings of or and and. Code like (x == "Y") | (x == "y") even seems to prove it — it works.

✓ The reality

They're bit-column operators that merely coincide with logic on plain bools. Two differences bite. First, they evaluate both sides, always — no short-circuit, so the crash-guard trick you just learned with and/or silently fails. Second, they bind tighter than ==, which is the real reason that line needs its parentheses: without them Python reads "Y" | x first and raises TypeError. Logic on truth values: and/or. Arithmetic on switches: &/|. Never reach for one meaning the other.

You now command a dozen-plus operators, and nothing stops you from stacking five of them in one breathless line: 1 | 2 ^ 1 & 3. Python answers 3 — but why that, and not something else? A new question has appeared: when operators collide, who binds first? →

Wait — XOR is its own undo. x ^ k ^ k is always back to x, because flipping the same switches twice returns them home. That one fact is the seed of the simplest cipher there is — XOR a message by a key, XOR again to decode — and it lets you swap two variables with no temporary at all: a ^= b; b ^= a; a ^= b leaves a and b traded.
A SHIFT THAT NEVER OVERFLOWS
Each left shift doubles: 1 << 10 is 1024, 1 << 20 is 1,048,576 (a megabyte). And because a Python int has no fixed width, 1 << 1000 is a real, exact integer — 302 digits long — computed without a blink. A 64-bit C long would have overflowed to 0 about 936 shifts earlier; Python just grows the number more switches.
Wait — the switches were never a metaphor. bin(200) returns '0b11001000' — the exact byte chapter 0 built by hand, one column at a time — and int('11001000', 2) walks it right back to 200. bin() and int(s, 2) are the front door to the row of switches every int has been hiding all along.
One int, N flag bits — capacity explodes, cost stays flatinteractive
one int, some one-bit flags — how many distinct states can it name, and what does each flag cost? 8 FLAG BITS = 2^8 states DISTINCT STATES THIS ONE INT CAN NAME 256 ≈ 256 states — it all fits inside one byte AND THE COST TO SET · TEST · TOGGLE ANY ONE FLAG? 1 operation one & , | , or ^ — no matter how wide the int THE INT — ONE SWITCH PER FLAG 8 of 64 switches live Each bit DOUBLES the states — but flipping any flag is still one instruction. The cost never moves.
8
Drag from 1 bit to 64. The number of distinct states an int can name doubles with every switch you add — yet set (state | flag), test (state & flag) and toggle (state ^ flag) each stay a single operation the whole way. Slide to 64: one machine word already names more configurations than there have been seconds since the Big Bang.
Fig 05e — Capacity is exponential, cost is flat. Each switch you add doubles the states one int can hold while the price to flip any one flag never leaves 1 — the whole reason a single integer can carry an entire configuration.
Trace3 step the machine — idea · code · memory move together
Three yes/no facts riding in one int
OR sets a bit and AND tests one, so several independent true/false flags pack into a single integer and read back column by column — exactly how permissions and packets travel. Press Next and watch the lamps, the register and the code move as one.
3 flagsbase 2bits 1·2·4
Three flag lamps beside a live binary registerint · 3 bitsinit
state
In plain words
Under the hood
SETALU · the operation
Program · flags.py
variables · type
Memory · state + the three flag masks
registers — the running state
state int · one packed field of flags
state & LOVED→ 4truthy · set
state & DOWNLOADED→ 0falsy · clear
what's happening
beat 1 / 1

06Who binds first: precedence

Type 2 + 3 * 4 and Python answers 14, not 20, and it is not being clever or careless. It is obeying a fixed pecking order: * outranks +, so the multiplication gets wrapped in invisible parentheses and settled first, as if you had written 2 + (3 * 4). Every operator you met in this chapter carries a rank — its precedence — and before a single value is computed, Python uses those ranks to decide the shape of the expression: who groups with whom. Get the shape right and the answer falls out for free.

That shape is a ladder, and Python consults the exact same ladder every time. The good news is you do not memorise all thirteen rungs. You memorise two things: the rough shape — arithmetic binds tighter than bitwise, which binds tighter than comparisons, which bind tighter than the words and/or — and the three surprises that trip up everyone who guessed. For everything else, reach for a pair of parentheses and stop worrying.

THE ONE IDEA TO CARRY FORWARD
Precedence decides grouping, not the order values pop out. It quietly drops parentheses into your expression to build a tree2 + 3 * 4 becomes 2 + (3 * 4) — and it does this while your code is still being compiled, before anything runs. Read precedence as "what clings to what," never as "what happens first."
The precedence ladder — tightest at the topfigure
binds TIGHTEST — grouped first ( ) parentheses — the manual override ** power (groups right → left) -x +x ~x unary minus, plus, bit-flip * / // % multiply, divide, floor, mod + - add, subtract << >> bit shifts & bitwise AND ^ bitwise XOR | bitwise OR == != < <= > >= is in not Boolean NOT and Boolean AND or Boolean OR binds LOOSEST — grouped last ① ** outranks the minus sign, so -2 ** 2 = -4 ② the bitwise family sits ABOVE the comparisons — so | binds first, not or ③ not ▸ and ▸ or, so True or False and False is True
Fig 06 — The precedence ladder. Higher rungs group first; families are colour-coded (arithmetic gold, bitwise red, comparison cyan, boolean green). On the comparison rung sits one operator this chapter hasn't built yet — in, the membership test song in playlist you'll meet in chapter 6; it binds at the same level as ==. The three circled surprises are the only rungs anyone actually gets wrong. When in doubt, add parentheses and stop doubting.

Surprise one lives at the top. ** outranks the unary minus, so -2 ** 2 reads as -(2 ** 2) = -4, not (-2) ** 2 = 4. The minus ranks below the power. Surprise two is the one that ends an old argument: the whole bitwise family — &, ^, | — sits above the comparisons, so in x == 1 | y == 2 the | grabs its neighbours before either == does. It is the final nail in the "| is just a fancier or" myth: | is nothing of the sort. Surprise three lives at the bottom: not beats and beats or, so True or False and False is True — the and runs inside the or, giving True or (False and False).

precedence.pypython
print(2 + 3 * 4)                 # 14  — * binds before +
print((2 + 3) * 4)               # 20  — parentheses rewrite the tree
print(-2 ** 2)                   # -4  — ** binds before unary minus
print(2 ** 3 ** 2)                # 512 — ** groups right-to-left
print(True or False and False)   # True — and binds before or

Reading the ladder is one thing; watching a real line collapse down it is another. Below is a genuine expression built from the playlist — is "Blinding Lights" (200 s) worth keeping? — and it packs six operators across four families. Drag the step control and watch Python fold it up one rung at a time, tightest first. The gold band shows which sub-expression binds at each step; the bottom line is the whole expression after that fold.

Step down the ladder — one binding at a timeinteractive
one line, six operators, four families — fold it tightest-first 200 + 45 * 2 > 180 and not 200 == 245 Python reads the whole line, then works the tightest rung first the line so far 200 + 45 * 2 > 180 and not 200 == 245
step 0 / 6
Push it rung by rung: 45 * 2 first (multiply is tightest), then +, then the two comparisons, then not, and and dead last. Notice the ladder never jumps around — it always drains from the top.
Fig 06b — The same line, folded tightest-first. Each step wraps one more rung in invisible parentheses; six folds later the whole expression is a single True. That fold order is the ladder.
One rung climbs backwards
Almost every operator leans left: 8 / 4 / 2 is (8 / 4) / 2 = 1.0, worked left to right. Power is the lone rebel — chained ** leans right: 2 ** 3 ** 2 is 2 ** (3 ** 2) = 2 ** 9 = 512, not 8 ** 2 = 64. When two operators share a rung, this left-or-right tie-break is called associativity, and ** is the one you have to remember.
Wait — if every comparison shares one rung, what does 1 < x < 5 even mean — (1 < x) < 5? No. Python spots a run of comparisons and quietly chains them with an and, evaluating the middle once: 1 < x < 5 is 1 < x and x < 5. Elegant when you want it — and the very mechanism behind surprise two. x == 1 | y == 2 is not "compare, then or"; it is the chain x == (1 | y) == 2, because | dived under both ==s first and left a run of two comparisons behind.
↻ reframe
Precedence isn't about time — no operator is racing to go first. It's about grouping: a set of invisible parentheses the language drops in for you before your program even runs. So stop asking "what happens first?" and ask "what clings to what?" The tighter an operator's rung, the more greedily it grabs the values on either side of it — and a greedy * will snatch the 3 out of 2 + 3 every time.

✗ The myth

"| is basically or with a sharper haircut, so x == 1 | y == 2 asks whether either side is true."

✓ The reality

| is bitwise OR, and it binds tighter than every comparison. So that line groups as x == (1 | y) == 2 and quietly becomes a chained comparison — a trap, not a question. When you mean logical OR, write or; when you mean "either of these comparisons," parenthesise: (x == 1) or (y == 2).

PARENTHESES ARE FREE
Grouping is resolved when your source compiles to bytecode (chapter 0), so parentheses add zero runtime work — (total / count) > 180 compiles to the byte-for-byte identical instructions as the bare version. The only thing they change is whether the next human to read your code has to rebuild the ladder in their head. Spend them without guilt.
The deeper cut — precedence is grouping, not the order things run

Throughout this section we have said Python "works the tightest rung first." That is a labelled simplification — the right picture for grouping, but not literally the order values are produced. Precedence and its partner associativity do just one job: they build the tree. They decide that 2 + 3 * 4 means 2 + (3 * 4) and that 2 ** 3 ** 2 means 2 ** (3 ** 2). That's all. Once that tree exists, Python walks it, and the runtime rule is a different rule: operands are evaluated strictly left to right, and and/or short-circuitTrue or slow() never even calls slow(), because or already has its answer from the left.

There is one more twist worth keeping now, because you will lean on it constantly later: and and or do not return True/False at all — they return one of the operands. 0 or "default" is "default"; 245 and 200 is 200. Precedence told you how the pieces group into a tree; short-circuiting decides which pieces of that tree ever run. Two different questions, two different answers, and confusing them is the last precedence bug you will ever chase.

An expression computes a value — but it never decides anything. Staple a decision onto it — if seconds > 180: — and the very same ladder you just learned suddenly steers which lines run at all. That is where your code starts to branch, loop, and react: the Control flow chapter →

Wait — Python has no ++. Type ++x and it's accepted but does nothing — it parses as +(+x), two unary pluses that hand back x unchanged. --x is the same no-op (minus a minus). And x++? A flat SyntaxError. C programmers write a silent no-op on muscle memory; to actually increment you reach for the += 1 from this chapter's first section.
not BINDS LOOSER THAN ==
not 200 == 245 reads as not (200 == 245)not FalseTrue, not as (not 200) == 245. Because not sits near the bottom of the ladder, the comparison happens first and not flips the finished verdict. Obvious once you see it, a misread waiting to happen until you do.
THE MINUS-SQUARED TRAP
** outranks the unary minus, so -2 ** 2 is -(2 ** 2) = -4, never (-2) ** 2 = 4. Fine as trivia — costly in a formula: write -x ** 2 in a variance or a distance and you get -(x**2), which is always ≤ 0, a sign bug that hides until x is what matters. Mean it? Parenthesise: (-x) ** 2.
PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 04, in working code

Operators are the verbs of arithmetic — small symbols that turn values into new values. Here we watch each one work in slow motion, from 445 // 60 becoming 7:25 to a boolean that quietly hands back the operand it stopped on.

The seven arithmetic operators
Add, subtract, multiply, and the three that trip people up: true-divide, floor-divide, and modulo. Every one here is a pure computation on two numbers.
Precedence & grouping
One expression, several operators — Python resolves them in a fixed order. Parentheses are the override.
Comparisons & booleans
Operators that answer yes-or-no — and two that behave more subtly than they look: chained comparisons, and the short-circuiting and/or.
Strings as operands
The arithmetic symbols do double duty on text: + joins, * repeats, and in asks about membership.
Formatting, augmentation & divmod
The expressions that shape output and mutate accumulators — f-string format specs, the augmented operators, and the two-in-one divmod.
end of chapter 04 · six sections
iolinked.com
Written by Ajai Raj