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.
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.
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.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.
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.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.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.
# 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) # 1That 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.
plays += 1 actually movesfigureplays += 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.
+= 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.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:
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) changesSo 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. →
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.:= 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.+ 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.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.
/ 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.// 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:
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 05Here'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.
// 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.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.// 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.%. 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.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.
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.// 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.// 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.
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. →
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.2**53 — 9,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.// 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.-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.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.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.
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.
# 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== 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 object — is 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.
is answers True — by luck. Cross 256 and they split into two objects; is flips to False while == never budges.== 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.)is on numbers or stringsx 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.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.== 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.== 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. →
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 True — is checks the object, not the value, so the two questions split clean apart right here.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.'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.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.'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.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.
"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.
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:
# 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 runsWhen 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.
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.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.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 & | ^ ~ << >> →
[] 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.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.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.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.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.
12 & 10 straight down the columnsfigure12 & 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.
a = 12, b = 10 under each; then set both sliders equal and watch ^ go to zero — a number XORed with itself is always 0.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.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 & DOWNLOADED → 0, 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.
-3 on an odd value like 5 and watch bits tumble off the right edge — that's flooring, live.& 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) & 0xFF → 94. 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, 1↔0. 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.
~12 is -13, not 3figure~12 come out negative, exactly at -x-1.~ 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.
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.# 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? →
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.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.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.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.1 — the whole reason a single integer can carry an entire configuration.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.
2 + 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."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).
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 orReading 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.
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.True. That fold order is the ladder.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.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.* 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).
(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-circuit — True 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 →
++. 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 200 == 245 reads as not (200 == 245) → not False → True, 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.** 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.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.