x = 42
x, type(x), id(x)(42, int, 139689282550856)
Everything in Python is an object. A variable is not a box that holds a value — it is a label attached to an object. And Python asks “what can this do?” (protocols), never “what is this?” (types).
In this chapter you will learn to:
for loop works on a list, a string, and a file.Most Python tutorials skip the mental models and dive into syntax. We don’t, because the models you build in the first hour decide how every later chapter lands.
Every object Python creates carries three pieces of information: its value (what it contains), its type (what kind of thing it is), and its identity (its address in memory). These three together explain almost every “why does this work that way?” question in the language.
To inspect them, Python gives us two built-in functions you can call on any object: type(x) returns the class the object belongs to, and id(x) returns its identity (a number unique while the object is alive). Together with the value itself, these are the three answers:
x = 42
x, type(x), id(x)(42, int, 139689282550856)
Walking through what we just asked Python to tell us:
x is the value — 42 itself.type(x) is the type — int, the class the object belongs to.id(x) is the identity — a number unique to this object during its lifetime. In CPython (the reference interpreter, written in C, that you almost certainly have installed), id returns the object’s memory address. Other implementations (PyPy, MicroPython, …) use different schemes, but the uniqueness while alive guarantee holds everywhere.Now the same three questions on two lists that look the same:
a = [1, 2, 3]
b = [1, 2, 3]
a == b, a is b(True, False)
a == b asks value equality — do they contain the same items? Yes.a is b asks identity — are they the same object in memory? No: each [1, 2, 3] literal builds a fresh list.The general rule: == is for value, is is for identity. They are not the same question. The deep treatment is in Chapter 18.
Most languages teach variables as boxes: x = 42 puts 42 into a box named x. Python doesn’t work that way, and the distinction matters. Assignment does not copy a value into a box — it creates an object and ties a name tag to it.
x = 42
y = x
x, y, x is y(42, 42, True)
x = 42 builds the integer 42 and binds the name x to it.y = x does not copy 42. It binds y to the same object x already points at.x is y asks the identity question: yes, both names refer to the same object.This single idea explains most of Python’s surprises: why a list modified through one variable shows the change through another, why functions can mutate the objects you pass them, and why mutable defaults are a trap.
xs = [1, 2, 3]
ys = xs
ys.append(4)
xs[1, 2, 3, 4]
ys = xs makes ys a second label on the one list, no copy.ys.append(4) mutates that single list in place.xs shows the same change, because there’s only one list to read.The general rule: assignment binds names to objects; mutation through any name is visible through every other name pointing at the same object. We revisit this in detail in Chapter 18.
The picture to keep in mind is two name tags tied to one heap object — for the list example just above:
flowchart LR
xs((xs)) --> obj["[1, 2, 3, 4]<br/>type: list<br/>id: 0x..."]
ys((ys)) --> obj
Both xs and ys are labels — they are not boxes that each hold their own copy of the list. They point at the same object, which is why the append through one name was visible through the other.
Python rarely asks “what type is this object?” It asks “can this object do what I need?” That question is answered by protocols — sets of special methods like __iter__, __len__, __getitem__.
A for loop doesn’t care if the thing being iterated is a list, a string, a file, a dictionary, or a custom class — it asks only “can I iterate over you?”
def first_two(thing):
seen = []
for x in thing:
seen.append(x)
if len(seen) == 2:
break
return seen
first_two([10, 20, 30]), first_two("hello"), first_two({"a": 1, "b": 2, "c": 3})([10, 20], ['h', 'e'], ['a', 'b'])
first_two never checks thing’s type. It asks for x in thing: — that’s the protocol question.list answers yes and yields its items. A str answers yes and yields its characters. A dict answers yes and yields its keys.The general rule — duck typing: if it walks like a duck and quacks like a duck, it’s a duck. Code that asks a question about behavior (can you be iterated?) works on every type that answers correctly, even types that didn’t exist when the function was written. The deep dive into the protocols themselves is the entire spine of Part I — starting with Chapter 13.
Most languages mark blocks with { and } (or begin/end) and treat indentation as cosmetic. Python flips that: indentation is the syntax. There are no braces; the parser reads the indent level to know where a block begins and ends.
def classify(n):
if n > 0:
return "positive"
elif n < 0:
return "negative"
return "zero"
classify(7), classify(-3), classify(0)('positive', 'negative', 'zero')
def classify(n): opens a function. Everything indented one level beneath is the function body.if n > 0: opens a conditional. Its body is the line indented one further.return "zero" is dedented back to the function level — it runs only when the if/elif chain falls through.The general rule: the body of any block (if, for, def, class, with, try) is whatever sits at the next indent level. Mixing tabs and spaces inside one block is a syntax error — pick four spaces and stay there.
Three mental models — objects with value/type/identity, variables-as-labels, and protocols over types — are load-bearing for the rest of the book. Almost every “surprise” in Python traces back to one of them. Internalize these now and the data model in Chapter 13 becomes obvious instead of magical.
This chapter only sketches identity vs. equality and protocols. The full story arrives later: identity and aliasing in Chapter 18, the data model and special methods in Chapter 13, and the iterator protocol specifically in Chapter 29.
The three properties — value, type, identity — and the duck-typing rule are easier to feel with a small program that prints them. We’ll build a describe(x) function that reports what Python knows about any object, then use it to watch aliasing happen.
Step 1: report the three properties. Take any object, return its value, its type name, and its identity:
def describe(x):
return {
"value": x,
"type": type(x).__name__,
"id": id(x),
}
describe(42){'value': 42, 'type': 'int', 'id': 139689282550856}
type(x).__name__ is the class name as a string ('int'); id(x) is the identity (a number unique while the object is alive). Together with the value, these three answer “what is this object?” the way the chapter framed it.
Step 2: ask a protocol question — is it sized? Some objects answer “how many” (list, str, dict); others don’t (int). We don’t branch on type(x); we try len(x) and let the protocol decide:
def describe(x):
info = {
"value": x,
"type": type(x).__name__,
"id": id(x),
}
try:
info["length"] = len(x)
except TypeError:
info["length"] = None
return info
[describe([1, 2, 3])["length"], describe("hello")["length"], describe(42)["length"]][3, 5, None]
Three different types, one function. The list and the string each answer the len question; the integer raises TypeError and we record None. That’s duck typing in code — no isinstance, no type hierarchy, just “can you do this?”
Step 3: see aliasing. With describe in hand, the variables-as-labels claim becomes visible. Two names tied to the same object share the same id; two separate literals don’t:
xs = [1, 2, 3]
ys = xs
zs = [1, 2, 3]
[describe(xs)["id"] == describe(ys)["id"], # same object
describe(xs)["id"] == describe(zs)["id"]] # equal value, different object[True, False]
xs and ys are two labels on the same list — ids match. zs is a fresh [1, 2, 3] literal, a different object that happens to be == to the others.
The whole inspector is fifteen lines and exercises three of the chapter’s four ideas: value/type/identity, duck typing through len, and aliasing through id. It’s also genuinely useful — when a Python program surprises you, dropping describe(x) into the call site usually shows why.
Same value, different identity. Create two lists a = [1, 2, 3] and b = [1, 2, 3]. Confirm a == b is True and a is b is False. Explain in one sentence.
Aliasing. Set x = [10, 20, 30] and y = x. Run y.append(40). Print x. Predict and explain.
Duck typing. Write a function count_a(thing) that returns how many 'a' characters a thing contains. Make it work on a string and on a list of strings using only for and ==.
Indentation. Re-format a small block of code by indenting an extra level inside an if. What error appears at parse time vs. run time?
Python is built on a small number of consistent rules: every value is an object with identity, type, and value; names are labels, not containers; behavior comes from protocols, not type hierarchies; indentation is syntax. The next chapter, Chapter 2, turns these rules into concrete primitives: int, float, str, bool, and None.