python/volume-1/chapter-02ch 02 / 64 · 18 min

02Everything is an object

In Chapter 1 we made the machine run our code. We shipped a program that printed our two-song playlist and controlled every byte on the way to the screen. But we never stopped to ask what those values actually are. That's this chapter. Here's the plan: we type one honest line — seconds = 200 — and pull it apart into the three separate moves Python really makes. Folded into that one = is the idea the whole language is built on. The question we keep coming back to is the one most tutorials skip straight past: when you write 200, where does its type live, and where do its behaviours live? The answer is the same for every value you will ever type. Each one is an object — a small parcel carrying its own value, its own type, and its own set of moves. By the end you'll know all six core value types: the three kinds of number, the booleans, that deliberate None, and frozen text versus raw bytes. And you'll be able to hand Python any value and have it tell you what it is, everything it can do, and exactly how many bytes it weighs.

One line, three moves — everything is an object Three kinds of number True, False, and a deliberate nothing Text is frozen; bytes are raw
scroll to begin

01One line, three moves — everything is an object

Let's start with the smallest line that carries the whole idea. It's one honest fact about the first track on our playlist — "Blinding Lights" by The Weeknd, 200 seconds long — typed the way you'd type anything. Then we turn around and ask Python what it just made:

first_object.pypython
seconds = 200              # the length of "Blinding Lights"
title   = "Blinding Lights"

print(type(seconds))     # <class 'int'>
print(type(title))       # <class 'str'>

It reads like a single action — "put 200 into seconds." But watch closely, because Python actually makes three moves, and their order is the whole secret. Move one, the object. Somewhere in RAM — chapter 0's street of numbered boxes — Python builds an object: a small package holding the value 200 and a single pointer to its type, int. That one pointer is the object's whole claim to behaviour. The methods are not copied into the parcel; they live once on the shared int type, and every integer looks them up there on demand. Move two, the name — and move three, the binding. The = creates the name seconds and clips it onto that object in a single stroke. CPython does both as one atomic step (STORE_NAME), so there is no real instant you could catch the tag hanging empty. We split it into two beats only to keep the order visible: the object is built first, and the label is stuck on afterwards. So = never manufactures the value; it only points a label at one that already lives somewhere.

THE ONE IDEA TO CARRY FORWARD
In Python, everything is an object — not "most things," everything. 200, "Blinding Lights", True, even nothing-at-all. Each is a bundle of value + type + behaviour travelling together through memory. A name is never the thing; it's a tag you clip onto the thing. Get that split clear and half of Python stops being surprising.
One statement, revealed one move at a timeinteractive
move 1 — object built in RAM int object 200 type: int bit_length() · to_bytes() · … lives at 0x7ff9…5298 move 2 — name created seconds move 3 · = binds Move 1 — Python builds the object: a value, its type, its behaviours. No name yet.
1 / 3
Slide left to right. The object is fully alive at move 1 — before the name exists. That ordering is the point: = clips a label onto a value it never had to create.
Fig 01 — seconds = 200 is three moves, not one. Object first, name second, binding last. The name is a tag; the object is the thing.

Two sibling rules follow from the same axiom. Python is case-sensitive: Title and title are two unrelated names. Asking for one you never bound is an instant error, not a shrug —

names.pypython
title = "Blinding Lights"
print(Title)      # NameError: name 'Title' is not defined

And indentation carries meaning: the blank space at the start of a line is grammar, not decoration, as chapter 5 makes precise. Both rules come from the same discipline. Python takes your words literally and binds them exactly.

Wait — if the object is built before the name, how could Python possibly know the order? Peek at the bytecode it compiles seconds = 200 into and you can read the two moves in sequence: LOAD_CONST 200 (make/point at the object) comes first, then STORE_NAME seconds (create the name and bind it). The interpreter literally does object-then-name — it's not a metaphor.
↺ the type belongs to the value, not the variable
Coming from C or Java, you picture a variable as a typed box — int x; — stamped once and fixed for life. Python is the reverse. The name is typeless; the object carries the type. So track = "Blinding Lights" and, a line later, track = 200 is perfectly legal — track was never an int or a str, it was just a label, free to re-clip onto any object you like. Each object knows what it is; the name only remembers where it last pointed.
One name, five objects — watch the type ride in on the valueinteractive
the name never changes track the object — and its type — changes str "Blinding Lights" type(track) → <class 'str'>
str
The box on the left holds still. Every slide drops a different object into the box on the right, and the type badge follows the object. track was never typed — it can hold a str, then an int, then nothing at all.
Fig 02 — Dynamic typing, made visible: the name is a constant tag, the type is a property of whatever object it currently points at.

✗ The myth

"A variable has a type. Once track holds a string it's a string variable, and putting a number in it is a type error waiting to happen."

✓ The reality

A variable has no type at all — it's a bare name. The type lives on the object it points to. Rebinding track to a number isn't an error; it's just aiming the same tag at a different, differently-typed object.

So the founding axiom pays off immediately. Because every value is a full object that knows its own type, you never have to declare types up front — you just ask any value what it is. That axiom reaches further than it looks. The type int is itself an object (its type is type); print is an object; a whole module is an object. It really is objects most of the way down, bottoming out at type, whose own type is type. For this chapter, though, we only need the everyday values, and here is the map of them.

The built-in types — this chapter, and what waits laterfigure
single-value types · covered in this chapter int float complex bool a subtype of int str bytes NoneType container types · arriving in chapters 5–7 list tuple range dict set frozenset bytearray memoryview
Fig 03 — Every built-in type. This chapter takes the single-value types up top; the containers below wait for chapters 5–7. All of them are objects; all of them play by the three-moves rule you just watched.
The deeper cut — three simplifications worth refining

Three things above were true at the level I pitched them, and each one sharpens on a second look.

"The name holds the object's address." That's the CPython picture, and a good one: id(seconds) really does hand back 140711682527896 (0x7ff9…5298), the object's location in memory. But the language only promises that id() returns a number that is unique and stable for the object's lifetime. A different interpreter could implement names and identity another way entirely. The guarantee is that the name binds to the object; "address" is just how CPython does it.

"200 fits in one byte." Chapter 0 packed 200 into a single byte, and that byte still lives inside this object. But sys.getsizeof(200) reports 28 bytes, not 1. That's because a Python int is a full object wrapped around the value: a pointer to its type, a reference count, and the digits themselves. The number now travels with luggage. That overhead is exactly the price of "everything is an object," and chapter 3 shows where it goes.

"A stack of behaviours." Run dir(int) and more than seventy names hang off the type. Most aren't methods you'd call by hand. They're the hidden hooks behind operators (__add__ for +, and so on), which chapter 4 opens up. The handful you would reach for, like (200).bit_length() giving 8, are the visible tip of that machinery.

If every value is an object that knows its own type — what types are there? Python answers the question of "how many is 200" three different ways. Start with the numbers →

EVEN print IS AN OBJECT
If "everything is an object" is literal, then print is one too — so you can clip a second name onto it: p = print, then p("hi") works exactly like print("hi"). Ask type(print) and Python answers <class 'builtin_function_or_method'>. The tool you call every day is just another value with a type.
Wait — if the type int is itself an object, you can put it in a name and carry it around like any value. t = int, then t("245") hands back the number 245 — you just called a type through a different tag. Types can live in variables, ride inside lists, be passed to functions. The blueprint is as much an object as the thing it builds.
Wait — if every value has a type, what is the type of a type? type(200) is int; type(int) is type; and type(type) is… type again. The tower of "the type of the type of…" doesn't run forever — it loops back on itself at the very top. type is the one object that is its own type, the quiet floor the whole language stands on.
↺ reframe
A line like seconds = seconds + 45 looks circular — how can a name define itself? It can't, and object-first is why it never has to. Python builds the right side first: it reads the object seconds currently points at, makes a brand-new object for the sum, and only then re-clips the seconds tag onto that new object. The old value was never edited; the name simply moved. Assignment is always object-then-bind, never a name chasing its own tail.
Trace3 step the machine — idea · code · memory move together
One statement, three moves — and a name that re‑clips
A line like seconds = 200 is really three moves in order: build the object first, create the name, then bind the name to the object’s address — though creating and binding the name are one atomic machine step (STORE_NAME), split into two beats here only so you can watch the order. Because the type rides on the object, the very same name can later re‑clip onto a different, differently‑typed value.
3 moves · 1 lineint → strtype on the object
The namespace board — tags on the left, objects on the rightnamespace · RAMnew obj
names
seconds
title
objects in RAM
In plain words
Under the hood
NEWALU · the operation
Program · rebind.py
names · type
Memory · namespace + objects
registers — the running state
namespace — name → address
objects in RAM — addr · type · value · size
what’s happening
beat 1 / 1

02Three kinds of number

You already met the machine's atom in chapter 0: a switch, a byte, a running time of 200 spelled out in eight lit and unlit boxes. Now Python hands that pile of switches back to you wearing three different faces, and it matters which one you're holding. There is int for whole numbers, float for numbers with a decimal point, and complex for the ones with an imaginary part. They print almost the same and they add with the same +. Underneath, though, they could hardly be more different: one grows without limit, one lives in a fixed 64-bit box, one carries two coordinates at once. This section is about telling them apart by feel.

three_numbers.pypython
seconds = 200            # int     — whole: "Blinding Lights" runs 200 s
minutes = 200 / 60       # float   — 3.3333333333333335, the same song in minutes
z = 3 + 4j               # complex — j is Python's imaginary unit, never i

print((10).bit_length()) # 4   → 10 is 0b1010, four bits — not 32
big = 2 ** 100           # 1267650600228229401496703205376
print(big.bit_length())  # 101 → still exact; a Python int never overflows

length = int("245")      # int() casts the text "245" → the number 245
print(seconds + length)  # 445 → it adds; "200" + "245" would have glued into "200245"

That last pair of lines is the one to watch. Chapter 1 warned you that input() always hands back text, even when the user typed digits. And text refuses to do arithmetic; it only glues. int("245") is the cure: it casts the characters into an actual number, and suddenly + means add instead of concatenate. Half of all beginner bugs are a number still wearing its text costume.

THE ONE IDEA TO CARRY FORWARD
An int is exact and boundless — it stretches to fit any whole number your RAM can hold. A float is fast and fixed — always 64 bits, and therefore always a little bit approximate. You choose one or the other every time you write a number; that choice is the whole section.

Start with the boundless one. In most languages an integer is parked in a fixed box — 32 or 64 switches, no more — and a value that outgrows its box overflows: it wraps back to zero like a six-digit odometer rolling from 999999 to 000000. Python's int flatly refuses to wrap. When a number needs more room, the object simply grows: Python bolts on another chunk of bits and keeps counting, as far as your memory allows. That's why 2 ** 100, a number with 31 digits, prints back exactly rather than as some rounded approximation. It is also why you can total the raw byte counts of a whole music library and still land on an exact integer. A 4-minute track at 320 kbps already weighs about 9.6 million bytes, and ten thousand of them go far past a 32-bit box's four-billion ceiling without ever wrapping. Slide the control below and watch the difference for yourself: the fixed 8-bit box on top can only ever hold 0–255, so push past 255 and it wraps, while the Python int underneath just grows another box.

The box that overflows vs. the number that growsinteractive
value = 200 = 0b11001000 fixed 8-bit box — the C / Java / CPU-register way 128 64 32 16 8 4 2 1 1 1 0 0 1 0 0 0 ⤺ lost 8-bit box holds: 200 Python int — grows a box whenever the number needs one 256 0 1 1 0 0 1 0 0 0 …← Python int: 200 · bit_length = 8 · no ceiling
200
200 ("Blinding Lights") and 245 ("Titanium") both fit the fixed byte. Now drag past 256: the 8-bit box wraps to nonsense while the Python int calmly grows a ninth box. Notice the lower row only ever draws the boxes it needs — that is exactly what bit_length() counts.
Fig 1 — An overflow is not a Python thing. Fixed-width boxes wrap; a Python int grows instead, so its value is always exact.

That bit_length() lands us on a fact worth burning in, because a widely-copied note online gets it wrong: (10).bit_length() is 4, not 32. Ten in binary is 1010 — four significant bits — and Python keeps only those four. There is no hidden 32-bit box padding the number out with leading zeros. The lower row in the figure above literally draws four boxes for ten and stops. bit_length() tells you the smallest number of switches this value needs, which is why 2 ** 100 reports 101 and not some round machine width.

Floats are approximations — on purpose
Type 0.1 + 0.2 and Python answers 0.30000000000000004. Not a bug: one tenth has no exact form in binary, the same way ⅓ has no exact form in decimal (0.3333…). The 64-bit float stores the nearest value it can represent and the tiny leftover surfaces when you add. Perfect for song lengths and averages; never for money — reach for Decimal there.

Why can't the float just be exact too, the way the int is? Because it made the opposite bargain. A float is always the same 64 switches, no matter what number sits in it — 3.14, or 3×10³⁰⁰, or one tenth. That fixed size is the point: a fixed size means the arithmetic is wired straight into your CPU, so floats are very fast and never grow. The price is precision. Sixty-four bits can only name about 15–17 significant decimal digits and a fixed menu of fractions, so any value off that menu — like 0.1 — gets rounded to the closest one on it.

Two bargains: fixed box vs. growing chainfigure
float — one fixed 64-bit box (IEEE-754 double) 1 sign 11 exponent 52 fraction bits same 64 bits for 3.14 or 3e300 — fast, but only ~15–17 decimal digits, and it stops near 1.8e308 int — no box at all; a chain of 30-bit chunks that grows to fit 30-bit chunk 30-bit chunk 30-bit chunk + more, no ceiling… getsizeof(200) = 28 bytes · 2¹⁰⁰ needs 4 chunks = 40 bytes · each extra chunk adds 4
Fig 2 — A float trades precision for a fixed, CPU-fast box; an int trades speed for an exact, unbounded chain. Same + sign on the outside, opposite deals underneath.
Wait — if a Python int can never overflow, can a float? Yes — and this is the twist most people miss. Because the float's box is fixed, it has a genuine ceiling: push past roughly 1.8e308 and Python raises OverflowError: (34, 'Result too large'). So the "unlimited" type is the plain integer, and the "scientific" floating-point type is the one that hits a wall. The boundless one is the humble one.

The third face, complex, answers a need the first two can't. Some quantities are irreducibly two-dimensional and have to travel as a single value: the phasor of an AC voltage (a magnitude and a phase at once), one bin of an audio FFT (a real part and an imaginary part), or simply a point on the plane. You may go years without reaching for it, but when the problem has two axes, one number must carry both. Write 3 + 4j and you get exactly that: a single value holding a real coordinate and an imaginary one. Python uses j for the imaginary unit, not the mathematician's i (electrical engineers do the same, to avoid clashing with i for current). Ask a complex number about itself and it answers: (3 + 4j).real is 3.0, .imag is 4.0, and abs(3 + 4j) is 5.0 — the length of the arrow to that point, your old friend the 3-4-5 triangle.

Wait — why j and not i? Try 3 + 4i and Python won't even parse it — SyntaxError: invalid decimal literal — because 4i looks like a malformed number. The j has to be glued to a numeral (4j, 1j) for Python to read it as imaginary. It's not a variable named j; it's a suffix, like the 0b you saw on binary literals.
↺ reframe
Don't think "int and float are two ways of writing a number." Think two contracts. The int contract promises exactness, forever, and charges you memory and speed to keep it. The float contract promises constant size and CPU speed, and charges you a sliver of precision. A dot in your literal isn't cosmetic — it's you signing a different contract. 200 and 200.0 are the same value living under two different sets of rules.

✗ The myth

"Python integers must have a maximum — every language has INT_MAX, so somewhere around 2⁶³ Python's must silently wrap or break too."

✓ The reality

There is no INT_MAX for a Python int. 2 ** 1000 is a perfectly ordinary, exact value. The only ceiling is your machine's memory — and it's the float, not the int, that actually has a hard numeric limit.

The deeper cut — what "grows a box" really means in CPython

The growing boxes in Fig 1 are a labelled simplification; here's the machinery underneath. CPython stores an int as a small object header plus an array of 30-bit digits, each packed into a 4-byte slot. A one-digit integer like 200 weighs sys.getsizeof(200) = 28 bytes, and every extra 30-bit digit adds exactly 4 more. So 2 ** 100 (101 bits, four digit-slots) reports 40, and 2 ** 1000 reports 160. The value stays perfectly exact; only the object gets heavier. A float, by contrast, is that same header wrapped around a single raw IEEE-754 double: 1 sign bit, 11 exponent bits, 52 stored fraction bits (plus one implied leading bit, so 53 bits of precision — sys.float_info.dig is 15). That is why float arithmetic runs at silicon speed and never grows, and why 0.1 is really stored as 0.1000000000000000055511151231257827021181583404541015625, the closest 53-bit fraction to one tenth.

Three kinds of number, and yet Python is hiding a fourth in plain sight — one wearing the costume of a word. True is quietly worth 1, False is worth 0, and lurking beside them is a deliberate nothing that is not zero at all →

Wait — a googol10 ** 100, a 1 followed by a hundred zeros — is a perfectly ordinary Python int. Python prints all 101 digits exactly, no rounding, no "e+100", and the whole colossus weighs just sys.getsizeof(10**100) = 72 bytes. No Python ever "runs out of integer" — it only ever runs out of RAM.
Wait — a float can't even count reliably. Past 2 ** 53 = 9007199254740992 the gaps between representable numbers grow wider than 1, so 2**53 == 2**53 + 1 comes back True — the float genuinely cannot tell those two integers apart. The exact type and the approximate type disagree about whether two whole numbers are equal. That is exactly why money and counters live in int, never float.
THE DOT CHANGES THE DEAL, NOT THE VALUE
200 == 200.0 is True, yet they are different objects under different contracts. The plain int 200 weighs 28 bytes; the "fancier" decimal 200.0 weighs only 24 — the float is actually the lighter one, because its box is a fixed 8-byte double while the int drags a header sized to grow. Same number, two prices, and the smaller price buys the shakier precision.
Wait — how many switches does the number zero need? (0).bit_length() is 0 — not one, zero. Every other number lights at least one switch; zero is the single value that needs none at all, because there is nothing to turn on. It's the tidy edge case that proves bit_length() counts only the significant bits, never a fixed-width box.
Trace3 step the machine — idea · code · memory move together
Two bargains under one plus sign — the int that grows, the float that rounds
The same arithmetic runs two opposite contracts. An int keeps every digit exact by growing more memory; a float stays a fixed 64-bit box that rounds — and has a hard ceiling. Press Next and watch the growing chain and the fixed box move against each other.
int · arbitraryfloat · binary64CPython 3.12
Growing chain vs fixed box — two contracts, one operatorint · growsassign
int · growsbit_length 8 · 28 B
200
float · fixed boxceiling 1.8e308
1sign
11exponent
52fraction
In plain words
Under the hood
SETALU · the operation
Program · two_contracts.py
names · type
Memory · registers + object
registers — the running state
n int object
what's happening
beat 1 / 1

03True, False, and a deliberate nothing

Two sections back you watched whole numbers grow out of switches. Now meet the smallest number type of all — so small it holds exactly two values. Python calls it bool, and its two values are True and False, each spelled with a leading capital, always. That capital is not decoration. Python is case-sensitive, so true and TRUE are not booleans; they are just names the language has never heard of, and reaching for one is an instant error. A bool is a single yes-or-no: the software echo of the one bit you met at the very bottom of the machine.

Here is the twist that catches everyone. A bool is not its own separate kind of value — it is a flavour of integer. In Python's family tree, bool is a subtype of int. True is genuinely equal to 1 and False to 0, they occupy the same 28 bytes an ordinary small integer does, and they will do arithmetic without a word of complaint. True + True is 2. That's not a party trick or a coincidence: a boolean simply is a number, printed in friendlier ink.

THE ONE IDEA TO CARRY FORWARD
True is 1 and False is 0, all the way down. Every strange thing you are about to see — booleans that add up, conditions you can sum, a True that indexes a list — falls straight out of that one fact. A bool is an int that has promised to only ever be 0 or 1.
bools.pypython
# a bool is a flavour of int — same value, same 28 bytes
print(True == 1)                # True
print(isinstance(True, int))    # True  — bool IS a kind of int
print(True + True)              # 2     (yes, really)
print(issubclass(bool, int))  # True

# equal in value is not the same as being the same object
one = 1
print(True is one)             # False — == compares value, is compares identity

next_track = None              # nothing queued after "Titanium"
print(type(next_track))        # <class 'NoneType'>

So a bool and an int are cut from the same cloth, but Python still draws a clean line around the two of them and a third, lonelier value. The map below is worth a slow look: bool nested inside int, and off on its own, a type with a single inhabitant.

Where bool, int, and None sitfigure
object int … 200 245 0 -7 … bool True = 1 False = 0 isinstance(True, int) → True NoneType None one instance, shared program-wide a bare 16-byte header — smaller than int 0
Fig 03 — bool lives inside int: every True or False is a genuine integer (1 or 0). None stands apart in its own type, NoneType, whose only possible value is the single shared None.

Numbers and booleans are not the only things Python can weigh on this scale. Pour any value through bool() and it comes out as exactly one of those two. That squeeze is called truthiness, and in chapter 5 every if and while you write will lean on it. The rule is blunt and worth burning in: a value is falsy when it is empty, zero, or nothing0, 0.0, the empty string "", the empty list [], the empty dict {}, and None — and truthy in every other case. Drag through the zoo below and watch each value fall to one side.

Truthiness — squeeze any value down to one bitinteractive
bool(x) — every value lands on exactly one side the value x "0" str bool( ) result True = 1 non-empty — what the string spells is irrelevant empty · zero · None → False everything else → True
bool("0") → True
Walk the zoo. Watch "" and [] and None land on False, while "0", "False", and [200] all land on True — content never matters, only emptiness. The = 1 / = 0 line is the reminder that the answer is really an integer.
Fig 03b — Truthiness is a one-bit verdict: is this value empty/zero/nothing, or not? The characters a string contains — even the word "False" — never enter into it.

✗ The myth

bool("0") and bool("False") must be False — the text plainly says zero and false.

✓ The reality

Both are True. A string's truthiness asks one question only — is it empty? — and never reads a single character inside. bool("") is the only string that is False.

Which leaves the strangest value in the language: None. It is Python's deliberate nothing — not zero, not the empty string, but a real object whose whole job is to mean "no value here, and that is on purpose." Our playlist uses it honestly: next_track = None says the queue after "Titanium" is empty, which is a genuinely different fact from a track whose title happens to be "". Because None carries no data, no two Nones could ever differ, so Python builds exactly one at start-up and hands that same object to everyone who asks. That pattern is called a singleton. It is a bare object header — 16 bytes with nothing inside, smaller even than the integer 0 (which needs 28). Ask its type and Python answers <class 'NoneType'>: a type with exactly one possible value.

ALWAYS TEST None WITH is
Write x is None, never x == None. Since there is only one None in the entire program, is (same object?) is both faster and safer than == (equal value?) — and it can't be tricked by some object that has redefined what "equal" means.
Wait — if True is 1, does sum([True, False, True]) equal 2? It does — and that is quietly one of Python's neatest tricks. Ask "how many of my songs run past 210 seconds?" and you can write sum(sec > 210 for sec in lengths): each comparison yields a True or False, and summing them counts the Trues. (That for sec in lengths is a loop — its machinery arrives in chapter 5; for now just read it as "run the test on every length.") Counting, it turns out, is just adding up ones.
↺ reframe
Stop picturing True/False as a separate species from numbers. A boolean is a number that has agreed to only ever be 1 or 0 — which is why it slots into arithmetic, indexes a list (["off", "on"][True] gives "on"), and sums into a count. None is the mirror image: not a small number, but the pointed absence of one.
The deeper cut — when 1 and True are literally the same dict key, and why "True equals 1" is exact rather than loose

"True is 1" is not a friendly approximation you'll later have to unlearn; it is precise. But it is precise about value, not identity. True == 1 is True while True is 1 is False, because they are equal numbers stored as two distinct objects. Two operators ask two different questions: == asks "same value?", is asks "same object?".

Watch how far the equality goes. A dict — the value→value lookup you'll build in chapter 7 — files each key by first folding it to an integer called its hash. It then treats two keys as the same slot when their hashes match and they compare ==-equal. True hashes to 1 and compares equal to 1, so a dict genuinely cannot tell them apart. Write {1: "one", True: "boolean"} and it collapses to a single entry, {1: "boolean"}: the second key landed right on top of the first. Same story with 0 and False. There is no cleaner proof that a bool really is that integer.

And the "flavour of int" label has teeth in the other direction too. bool is so locked down that Python refuses to let you subclass it: class MyBool(bool) raises TypeError, because there must only ever be the two bool objects, True and False, in all of memory. A bit of history explains the whole design. Python had no bool at all until 2003 (version 2.3). Before that everyone used plain 1 and 0 for yes and no, so when the real type finally arrived it was deliberately made a subtype of int — a promise that not one line of the old code would break. That promise is exactly why True + True still equals 2 today.

Numbers, booleans, a deliberate nothing — all tidy, countable, and cheap. But a playlist is mostly text, and Python's text is carved in stone: you may read every character, yet never reshape one in place. Next we hold a string up to the light — frozen letters above, raw bytes beneath →

Wait — which weighs less, the number 0 or None? Nothing wins. sys.getsizeof(0) is 28 bytes; sys.getsizeof(None) is just 16 — a bare object header with no value inside. Python's deliberate nothing is literally lighter than its zero. Absence, it turns out, is cheaper to store than emptiness.
THERE IS ONLY ONE None, EVER
Every = None you will ever write — across every file, every library, a billion times over — points at the same single object Python builds once at start-up. That's what "singleton" means, and it's why the whole program spends a flat 16 bytes on None no matter how often you reach for it. You can't make a second one; even type(None)() just hands you the original back.
Wait — because a bool is 1 or 0, you can multiply by it to switch a value on and off with no if in sight. True * 200 is 200; False * 200 is 0. So plays * (length > 210) quietly zeroes out every song that fails the test and keeps the rest untouched — a whole branch collapsed into a single multiplication.
Wait — 0 and None both squeeze down to False, so are they the same? Not remotely — 0 == None is False. Truthiness asks one blunt question ("empty, zero, or nothing?") and both answer yes, but that shared verdict hides two utterly different facts: 0 is a real number you can add to, None is the pointed refusal to hold a value at all. Same side of the line, opposite meanings.
Trace3 step the machine — idea · code · memory move together
Counting is just adding ones — a bool is an int wearing a name
Each comparison hands back True (1) or False (0). Sum a column of them and you have literally counted how many passed — not one if in sight. (The walk down the column is a loop, built in chapter 5; here just watch the four verdicts land.) Press Next and watch each verdict get stamped and dropped into the tally.
4 songsgate > 210no if
The idea — a tally gate that stamps 1 or 0bool · a 1/0 intinit
> 210
over =
In plain words
Under the hood
SETALU · the operation
Program · count_over.py
variables · type
Memory · registers + object
registers — the running state
lengths list · 4 song-lengths in seconds
what's happening
beat 1 / 1

04Text is frozen; bytes are raw

A number knows nothing about itself. Back in chapter 0 you watched "Blinding Lights" laid across fifteen boxes in RAM, its first letter B stored not as a shape but as the bare number 66 — with a promise to explain later which code, and why. This is later. str is Python's text type, and the refinement is one sentence: a string is a sequence of Unicode code points. Unicode is the single table the whole planet agreed on. Every character that has ever been written — every Latin letter, every kanji, every emoji — gets one permanent number called its code point. B is 66, é is 233, the note 🎵 is 127,925. A string is just those numbers stood in a row, each still wearing its agreed-upon meaning.

And a string is frozen. Once Python builds one, nothing edits it in place — not you, not a method, not a single character of it. Every method that looks like it changes text quietly makes a brand-new string and hands that back, leaving the original untouched. That one rule — str is immutable — shapes half of what you'll do with text, so hold it close.

"Blinding" as code points, and as the bytes it becomesfigure
str — text, as Unicode code points (meaning attached) B 66 l 108 i 105 n 110 d 100 i 105 n 110 g 103 bytes — the very same numbers, meaning stripped, ready for the wire 66 108 105 110 100 105 110 103 for plain ASCII the code point and the byte are the same number — the two rows only split apart above 127
Fig 04a — str is numbers-with-meaning (code points); bytes is the same wire-level numbers with the meaning peeled off. For letters like these they line up exactly — which is why chapter 0 could say "B is 66" and move on.

That bottom row is the other sequence type in this section. bytes is a row of raw values 0–255 with no meaning attached at all — the parcels that files on disk, packets on a network, and every MP3 of the song are actually made of. str lives only inside Python. The moment text has to leave — hit a disk, cross a socket — it must be encoded into bytes. "Blinding".encode() gives b'Blinding'. Reach into bytes with an index and you get a plain int back: b'Blinding'[0] is 66, not "B". Indexing a str, by contrast, hands you a one-character string. Two sequences, two temperaments.

WHY BYTES EXIST AT ALL
Everything outside your program is bytes — the file, the network packet, the eight-million-byte MP3 of "Blinding Lights." A str is a comfort that exists only after Python decodes those bytes into code points. When files and sockets arrive in later chapters, bytes is the doorway you pass through — so meeting it now, quietly, pays off loudly later.

But the tidy one-number-per-letter picture in that figure only holds for plain ASCII. The moment a real playlist carries a name like Beyoncé, that accented é pushes past code point 127 and no longer fits in a single byte. The encoder spreads it across two, three, even four. Drag through a few characters below and watch one code point balloon into a parcel of raw bytes.

One code point in — its UTF-8 bytes outinteractive
one str character encodes into one to four bytes B code point 66 U+0042 .encode() 66 · · · one code point → 1 UTF-8 byte
B
Slide from B (an ASCII letter, one byte) up to 🎵 (a musical note far out in Unicode, four bytes). The character never changes; only how many bytes it costs on the wire does. This variable width is UTF-8 — the encoding Python uses by default.
Fig 04b — Encoding is the bridge from a code point to raw bytes. ASCII characters cross in a single byte; anything richer pays for its reach with more bytes.
Wait — if é is one character but two bytes, how does Python know those two bytes aren't two separate characters when it reads them back? Because UTF-8 is self-describing — and you can read the announcement right in the bits. Encode é and its two bytes come back as 195, 169, which in binary are 11000011 and 10101001. That leading 110 is a flag meaning "a two-byte character starts here"; the 10 on the front of the second byte means "I'm a continuation — more of the one before me." (A four-byte character like 🎵 opens with 11110 and drags three 10… continuation bytes behind it.) The boundaries are baked into the bytes themselves, so .decode() can always re-group them into the exact code points you started with — no lengths stored on the side, no guessing.

Now watch immutability in the open. Every string method returns a new string; the one you called it on is never touched. find reports an index, or -1 for "not here" instead of raising. And a direct edit — track[0] = "B" — is simply forbidden.

strings.pypython
# a str is a sequence of code points — and it never changes in place
track = "blinding lights"

print(track.title())                  # Blinding Lights   — a NEW string
print(track.find("titanium"))         # -1   — "not found" is -1, never a crash
print(track.replace("lights", "dark"))  # blinding dark    — also a NEW string
print(track)                          # blinding lights  — the original never moved

wire = track.encode()                 # b'blinding lights' — packed for the outside world
print(wire[0])                       # 98   — indexing bytes returns a number, not "b"
print(wire.decode())                  # blinding lights  — bytes read back into text

# track[0] = "B"    →  TypeError: 'str' object does not support item assignment
A method builds a new object — the original is untouchedfigure
"blinding lights" id …a3f10 (unchanged) .upper() "BLINDING LIGHTS" id …b7c48 — a brand-new object track[0] = "B" ✗ TypeError — a str is frozen; there is no in-place edit, ever
Fig 04c — String methods never rewrite; they build a fresh object at a fresh identity and return it. The name you called it on still points at the original.
GLUING TEXT TO NUMBERS: f-STRINGS
"Length: " + 200 raises TypeError: can only concatenate str (not "int") to str — Python refuses to silently guess whether you meant text or arithmetic. The fix is an f-string: f"Length: {seconds}s"Length: 200s. Anything inside the braces is turned into text for you, no manual conversion required.
↺ reframe
Stop picturing a string as a whiteboard you rub out and rewrite. Picture it as a developed photograph. .upper() doesn't scrub the print — it shoots a whole new photo and points your name at that. The old photo is still perfectly intact, and anyone else holding it sees the original, unchanged. Immutability isn't a restriction; it's the guarantee that a string you were handed can never be altered behind your back.

✗ The myth

"text = text.upper() changes the string" — and anyway a string is really just its bytes, so the two are interchangeable.

✓ The reality

The method builds a new string; the assignment just re-points the name. And a str (code points) is not its bytes (an encoding) — mixing them up is exactly how you get garbled é where an é should be.

The deeper cut — what a character really costs, and the O(n²) trap

CPython stores a string with a flexible representation: 1, 2, or 4 bytes per character, chosen by the widest code point in the whole string. All-ASCII "Blinding Lights" costs 1 byte each, so sys.getsizeof reports a 41-byte base plus 15, or 56 bytes. Append a single 🎵 and the entire string is re-stored at 4 bytes per character: those fifteen plain letters now cost 60 bytes between them, and the whole object jumps to 124 bytes. One emoji quadruples the price of every letter around it.

Immutability carries a second, sneakier cost. You'll be taught — correctly, as the rule to remember — that building text with s += piece in a loop is O(n²), because a frozen string can't grow: each += must copy everything so far into a new, bigger string. One honest refinement, so you're not surprised later: on CPython there's a special-case optimization that can sometimes grow the string in place and rescue that particular loop. But it's fragile — build the string by prepending, or hold a second reference, or run on a different Python, and the full quadratic copy comes back. The always-safe idiom is to collect the pieces and call "".join(...) once, which is linear no matter the runtime. Lists, and that join, arrive in chapter 6.

Seven types now, each frozen or fluid, each priced differently. So how do you walk up to a value you've never met — ask what it is, whether two of them are truly the same, and exactly what it weighs? →

Wait — reach into a bytes two different ways and you get two different types. b'Blinding'[0] is the plain int 66, but b'Blinding'[0:1] — the same first byte, taken as a slice — is b'B', a bytes object. One square bracket hands you a number, a colon inside it hands you bytes. The value never moved; only how you asked for it did.
Wait — two strings that look identical on screen — both showing é — can have different lengths. One is a single code point (len 1); the other is a plain e followed by a separate "combining accent" code point (len 2). They render pixel-for-pixel the same, yet == says they are unequal. Your eye sees a character; Python counts code points, and the two are not the same thing.
YOUR SOURCE CODE IS BYTES FIRST, TOO
The str / bytes split isn't only about files you open — it reaches back to the program itself. Python reads your .py as a stream of raw UTF-8 bytes and decodes them into text before it runs a single line. Type a 🎵 in a string literal and it lived on disk as four bytes first. Everything enters as bytes and becomes str only once it's safely inside.
Wait — "Blinding Lights".find("z") returns -1 for "not here" — and that -1 is a trap. Write if title.find("z"): to mean "if found" and it fires on failure, because -1 is truthy (only 0 is falsy) — and -1 is also a perfectly valid index, the last character. The safe test is if "z" in title:, which answers a clean True/False instead of a number pretending to be one.
Trace3 step the machine — idea · code · memory move together
From code points to the wire — one character, 1 to 4 bytes
encode() walks a string one code point at a time, and UTF-8 turns each into 1–4 self-describing bytes — an ASCII letter costs one, an accent two, a music note four. Each group's lead byte announces how many follow, so decoding back is exact. Press Next and watch the tape fill.
3 code pointsUTF-87 bytes out
The idea — encode walks each code point, the tape fillsstr → bytesinit
In plain words
Under the hood
SCANALU · the operation
Program · encode_utf8.py
variables · type
Memory · registers + object
registers — the encoder's running state
data bytes · the wire form — grows as encode walks
what's happening
beat 1 / 1

05Interrogate any value — and weigh it

Because every value in Python is an object — a little parcel that carries its own type and its own behaviour — you never have to guess what you're holding. You can ask it. Python hands you a small interrogation kit that works on anything: a number, a string, a song length, a value you've never seen before. It's four questions and a scale. type(x) asks "what are you?" isinstance(x, int) asks "are you this kind of thing?" dir(x) asks "what can you do?" id(x) asks "where do you live?" And sys.getsizeof(x) puts the object on a scale and reads off its weight in bytes.

Point the kit at the length of "Blinding Lights" — the number 200 — and it answers int, True, an address, 28 bytes. Point it at the title string and every answer changes, because the object is different. The value isn't looking anything up in a manual; it is the manual. That's the whole payoff of "everything is an object": the answers travel inside the thing you're asking about.

THE ONE IDEA TO CARRY FORWARD
You don't inspect a value by consulting some outside registry of "what type is this variable." You interrogate the object itself, and it reports its own type, abilities, address, and size. Every tool in this section is just a different question put to the same self-describing parcel.

Look closely at two of those questions and you'll spot Python's two shapes for "run some code on a value." len(title) is a function: a free-standing tool, and the object goes in as an argument. title.upper() is a method: behaviour that lives inside the str type itself, reached through the object with a dot. Same idea — code acting on a value — but a different address for the tool. Functions you hand the value to; methods the value carries with it.

Two shapes for acting on a valuefigure
a FUNCTION — the value goes IN len( "Blinding Lights" ) → 15 one free-standing tool, works on many types a METHOD — the tool rides INSIDE "Blinding Lights" .upper() .find() .split() .title() … 47 methods packed inside every str, reached with a dot title.upper() → "BLINDING LIGHTS"
Fig 05 — len(title) hands the object to an outside tool; title.upper() uses a tool the object carries with it. Free-standing function versus dotted method — two doorways to the same room.

Here's the whole kit fired at one string. Nothing to install, nothing to configure — just questions and answers:

interrogate.pypython
import sys
title = "Blinding Lights"

print(type(title))              # <class 'str'>   — "what are you?"
print(isinstance(title, str))   # True            — "are you this kind?"
print(isinstance(True, int))    # True            — a bool IS an int underneath
print(len(title))               # 15              — FUNCTION: the value goes in
print(title.upper())          # BLINDING LIGHTS — METHOD: reached with a dot
print(len(dir(title)))          # 81              — every name this str carries
print(id(title))                # 140399482229104 (varies) — where it lives
print(sys.getsizeof(title))     # 56              — its weight, in bytes
print(sys.getsizeof(200))       # 28              — one small int

That dir(title) is worth a pause: it spills out 81 names the string carries — 47 of them public methods like .upper(), .find(), .split(), and the rest the dunder machinery we'll meet later. You never memorise a string's abilities; you ask it for the list. And id(title) returns a plain integer that is, in CPython, the object's actual address in RAM — the house number from chapter 0, made visible. (That address-equals-id fact is a CPython promise, not a Python one: the language guarantees only that id is a unique, unchanging integer for as long as the object lives. Chapter 3 turns it into a superpower.)

Two names, one value — but not always one objectinteractive
a and b hold the same value — but is it the same object? Ask id(). a = 256  b = 256 SAME OBJECT? · a is b True identity — one object, or two? SAME VALUE? · a == b True value — always equal. NAMES a b THE OBJECT(S) IN RAM 256 ONE cached object · shared by every program 256 id(a) == id(b) = 0x556100 — literally one object −5 … 256 are pre-built once at start-up — 262 integers every program silently shares.
256
Start at 256a is b is True: both names land on the one integer object CPython pre-builds. Nudge to 257 and identity shatters — same value, two objects, a is b is False — while a == b never wavers. The cache covers exactly the 262 integers −5 … 256; it's a CPython optimisation, not a language rule. (Type the two lines at the REPL to see it — inside one compiled block the compiler may fold equal literals together.)
Fig 05a — Two names, one value — but not always one object. Up to 256, Python hands out a single cached int; past the cliff, every literal is a fresh object with its own address. Value equality never changes; identity falls off a cliff.

Now the scale. Every literal from this chapter has a price in bytes — step through them and watch the object announce its type, place itself in Python's family tree, and tip the scale:

The interrogation bench — step through the literalsinteractive
one bench — type() names it · isinstance() places it · getsizeof() weighs it 200 type = int is a: int object object isinstance(x, int) → True sys.getsizeof(x) 28 bytes object header + one 30-bit digit — every int up to ~10⁹ is exactly 28 B
200 — int, 28 bytes
Same playlist, wildly different price tags. Watch True place itself under int in the family tree — that's why isinstance(True, int) is True. Notice text costs per character, a float is a flat 24 B, and None is nearly free.
Fig 05b — One value at a time on the bench: its type, its ancestry (the green int pill lights whenever the value counts as an int), and its byte weight. The bar is sys.getsizeof.
Your bytes may vary
These weights are from CPython on a 64-bit machine (Python 3.12 here); a different version or build can shift them by a few bytes. Don't memorise the numbers — keep the shape: every object pays a fixed header tax first, then a per-content cost on top. That tax — 28 bytes just to hold the number 200 — is exactly why chapter 8's typed arrays exist, packing millions of numbers without paying the toll once each.
Wait — if True is really an int, is it just 1 in disguise? Essentially, yes. True == 1, True + True is 2, and ["off","on"][True] indexes position 1 and hands you "on". bool is a subclass of int that adds nothing but two pretty names and a tidy repr. That's not a quirk to fear — it's why counting how many songs pass a test can be as blunt as summing a list of Trues.
↺ reframe
Stop imagining a lookup table somewhere in Python that maps your variable to a type. There is no such table. The type, the size, the list of abilities, the address — the object is carrying all of it right now, and type, dir, id, getsizeof are just four different doors into the same parcel. Interrogation isn't Python looking your value up. It's the value answering for itself.

✗ The myth

To test what something is, compare its type: type(x) == int. If they match, done.

✓ The reality

That test silently misreads subtypes. type(True) == int is False — its type is bool — yet a bool is a kind of int. isinstance(x, int) returns True because it walks the family tree the bench just drew. Ask "is it this kind?", not "is it exactly this stamp?".

The deeper cut — what "weighs the object" quietly leaves out

Calling sys.getsizeof a scale is a labelled simplification, and here's the refinement: it weighs the object shallowly. Ask it for the size of a list of three song lengths, [200, 245, 222], and it answers 88 bytes. But that 88 is only the list's own bookkeeping — its header plus three 8-byte slots. It does not include the three int objects those slots point at; add their 28 bytes each and the true footprint is 172. The scale weighs the box, not everything the box refers to elsewhere in RAM — the same one-extra-hop layout chapter 0 hinted at for lists.

And the int isn't magic either. A small int is 28 bytes because it stores one 30-bit digit. Cross 2³⁰ and Python bolts on another 4-byte digit: sys.getsizeof(2**40) is 32, 2**70 is 36. Python integers never overflow precisely because they're allowed to grow one digit at a time. Strings scale the same honest way: a 41-byte base for the ASCII string object, then one flat byte per character. No mystery — just header plus content, every time.

You now have the tools to weigh and identify any object. But one thread from earlier is impossible to ignore: the value 200 was built first, and the name only points at its address. So what happens with b = seconds? Two names, one object — and id() can prove it. Chapter 3 follows the arrows into names & memory. →

Wait — if len() is a free-standing function and .upper() is a method, are they really different underneath? Not much — len(x) just turns around and calls the object's own x.__len__(). len("Blinding Lights") and "Blinding Lights".__len__() both give 15. The friendly function is a doorway to a method the value was carrying all along — which is exactly why len(200) is a TypeError: an int has no __len__ to call.
Wait — how much can a plain integer actually do? Run len(dir(int)) and Python lists 74 names hanging off it. Most are the hidden hooks behind operators — __add__ is what + secretly calls, __lt__ what < calls — so 200 isn't a dumb quantity; it's an object carrying 74 named abilities. You never memorise them. You ask dir().
Wait — put a truly enormous number on the scale. sys.getsizeof(200) is 28 bytes; sys.getsizeof(2**1000) — a 302-digit monster — is only 160. Barely six times heavier for a number unfathomably larger, because weight tracks the digit-chunks, not the value: a header plus a chain that grows four bytes at a time. The scale weighs the box, and the box stays almost small.
THE ONE QUESTION EVERY VALUE ANSWERS "YES"
Point the kit at anything and ask isinstance(x, object)200, "Blinding Lights", None, even int and type themselves — and the answer is always True. That single unfailing "yes" is this chapter's thesis, provable right at the prompt: there is nothing in Python that isn't an object. The interrogation kit doesn't just describe values; it demonstrates the axiom.
Trace3 step the machine — idea · code · memory move together
One kit, any value — type names it, isinstance places it, getsizeof weighs it
Because a value carries its own type, ancestry and size, one interrogation kit reads them straight off the object. (The kit's fourth question — id(), "where do you live?" — gets its own spotlight in chapter 3.) Press Next and watch isinstance walk a family tree that type == can't see.
5 valuestype · isinstance · getsizeofbool int
The bench — one value, three questions5 objectsinit
weight · sys.getsizeof
In plain words
Under the hood
SETthe kit · what it asks
Program · interrogate.py
variables · type
Memory · registers + object
registers — what the kit read
x the object on the bench — header tax + content
what's happening
beat 1 / 1
PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 02, in working code

In Python, nothing is a plain scalar — every value is an object with a type, an identity, and behaviour. These fifteen tiny programs let you watch that claim hold up, one line at a time.

Every value has a type
type(x) never lies — a string, an int, a float, and even a bool each report exactly what they are.
Identity versus equality
id(x) and is ask 'the same object?'; == asks 'the same value?'. They are not the same question.
Where a value fits — isinstance
isinstance asks whether a value belongs to a type, and it understands the type hierarchy.
Values have behaviour
Objects carry methods. Strings return new strings (they're immutable); lists mutate in place.
Nothing, truth, and functions
None means absence; truthiness collapses any value to True/False; and a function is itself an object you can bind and call.
end of chapter 02 · five sections
iolinked.com
Written by Ajai Raj