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.
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:
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.
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.= clips a label onto a value it never had to create.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 —
title = "Blinding Lights"
print(Title) # NameError: name 'Title' is not definedAnd 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.
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.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.track was never typed — it can hold a str, then an int, then nothing at all.✗ 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 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 →
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.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.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.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.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.
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.
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.
int calmly grows a ninth box. Notice the lower row only ever draws the boxes it needs — that is exactly what bit_length() counts.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.
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.
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.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.
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.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 →
10 ** 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.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.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.(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.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.
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.# 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.
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 nothing — 0, 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.
"" 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.✗ 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.
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.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.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 →
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.= 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.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.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.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.
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.
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.
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.é 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.
# 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"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..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? →
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.é — 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.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."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.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.
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.
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:
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 intThat 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.)
a 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.)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:
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.int pill lights whenever the value counts as an int), and its byte weight. The bar is sys.getsizeof.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.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. →
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.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().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.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.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.