Lesson 1 of 5
Names, Objects & Assignment
"Namespaces are one honking great idea — let's do more of those!" — Tim Peters, The Zen of Python (PEP 20, 1999)
Almost everyone arrives at Python carrying a mental model from somewhere else, and it is the same model: a variable is a box, and assignment puts a value in the box. It survives the first week because it predicts the right answers for numbers and strings. It breaks in week two, usually on a Tuesday, when a function you wrote appends one item to a list and something on the other side of the program changes.
Python does not have boxes. It has objects, which live in memory and know their own type, and it has names, which are labels you attach to those objects. Assignment does not fill a container; it ties a label to an object. Nothing is copied, nothing is moved, and one object can wear as many labels as you like.
That single change of picture explains a startling amount of Python. It explains why two variables can appear to change together, why += behaves differently on a list than on an integer, why reassigning a parameter inside a function accomplishes nothing while mutating it changes the caller's data, and why is and == are different questions rather than two spellings of one.
This chapter builds that picture properly, because everything in the rest of the volume rests on it. The data structures in Part II are objects with costs; the closures in Part III capture names, not values; the iterators in Part IV hand out references. Get the object model wrong and you will keep meeting the same confusion wearing new clothes.
You pass a list into a function. Inside, the function does items = items + ["extra"] and the caller sees no change. You change one character so it reads items += ["extra"] and now the caller's list has changed. Nothing about the call, the argument or the return value is different — so what exactly did that one character change?
A name is a label, not a box
When you write shopping = ["yam", "rice"], two separate things happen. Python builds a list object somewhere in memory, then binds the name shopping to it. The name is not the list and does not contain the list; it refers to it. Bind a second name and you have two labels on one object — not two lists.
id() gives you an object's identity: a number unique to that object for as long as it lives. You should never care about the value, but comparing two of them tells you whether you are looking at one object or two. The is operator asks exactly that question.
Click Run to execute the code.
There is no line in that block that copies anything. basket = shopping is often read aloud as "basket gets a copy of shopping", and that reading is the source of the bug it causes.
A person can be "Mum", "Dr Adeyemi" and "the woman in flat 4" all at once. Three names, one person — and if she moves house, all three names now describe someone living somewhere new, because none of the names contained her address. Nobody finds this confusing about people. Assignment in Python is naming, not boxing, and the confusion only appears because most languages taught us to picture a box.
Assignment never copies
The rule is short enough to memorise: assignment binds a name to an object; it never copies the object. Every aliasing surprise in Python follows from taking this literally.
This matters only for objects you can change. If the object is immutable — an integer, a string, a tuple — sharing it is undetectable, because there is no operation that could make the two names disagree. Sharing a mutable object is very detectable indeed.
When you genuinely want a second object, you must ask for one. list(original) or original[:] gives a shallow copy: a new outer list containing references to the same inner objects. That is enough when the contents are immutable, and quietly insufficient when they are not.
Click Run to execute the code.
The two-argument habit worth forming: when a function receives a mutable object it did not create, it either documents that it mutates it or it copies it first. Silent mutation of a caller's data is the most common way Python code becomes hard to reason about, and it is not a language flaw — it is a design decision the author declined to make.
Rebinding is not mutating
Here is where the box model does its real damage. Two operations look almost identical and are nothing alike:
- Rebinding points a name at a different object. The old object is untouched.
- Mutating changes the object itself. Every name bound to it sees the change.
x = x + 1 on an integer can only be rebinding, because integers cannot be changed — there is no way to make the number 5 become 6. So Python computes a new integer and moves the label. But lst = lst + [1] also builds a new list and rebinds, whereas lst += [1] calls the list's in-place add and mutates. Same-looking line, opposite semantics.
Click Run to execute the code.
That is the answer to the chapter's big question. items = items + ["extra"] builds a new list and rebinds the local name, leaving the caller's object alone. items += ["extra"] mutates the object the caller handed you. One character, and the difference is whether your function has a side effect.
When I read unfamiliar Python I mark every line that mutates a parameter, because those lines are the function's real signature. A function that takes a list and returns nothing is either useless or mutating, and knowing which takes one glance at the body. I have come to prefer functions that return new values even when mutation would be cheaper, not for purity but because the call site then tells the truth about what happened without my having to open the function.
Passing arguments is just assignment
Python's argument passing is often described with borrowed terms — "by value", "by reference" — and neither fits. What actually happens is simpler: each parameter name is bound to the object the caller passed. It is assignment, with the same rules you have just learned.
So a function cannot rebind a caller's name, because it only ever had its own name. It can mutate the shared object, because both names point at it. That one sentence predicts every argument-passing behaviour in the language.
Click Run to execute the code.
The behaviour has a proper name that predates Python: call by sharing, coined by Barbara Liskov for CLU in the 1970s and used by Java, Ruby, JavaScript and Python alike. Liskov's point was that the argument-passing debate of the era — by value versus by reference — assumed variables were storage locations. Once a variable is a reference to an object, the question dissolves: you share the object. Python inherited the model and, for a long time, not the vocabulary, which is why the "is Python by value or by reference?" argument still runs in forums where the answer has been settled since 1974.
is and == ask different questions
== asks whether two objects are equal — a question the objects answer themselves, via __eq__. is asks whether two names refer to one object. Equal objects are usually distinct; identical objects are always equal.
Almost always you want ==. The exception is the singletons, where identity is the point: None, True and False each exist exactly once per interpreter, so x is None is both the correct test and the fast one. Writing x == None works but invites a custom __eq__ to lie to you.
Where this gets genuinely confusing is CPython's caching. Small integers from −5 to 256 are pre-created and reused, and string literals in the same file are often interned, so is sometimes returns True for values you never deliberately shared. That is an implementation detail, not a language guarantee — and relying on it produces code that works on your machine and fails on another.
Click Run to execute the code.
Note the deliberate awkwardness in that block: writing n is 100 directly makes Python emit a SyntaxWarning, because the interpreter knows it is almost always a mistake. That warning is a feature. If you see it in your own output, the fix is ==.
Reference counting, and when objects die
CPython tracks how many references point at each object. When that count reaches zero, the object is freed immediately — which is why Python does not need you to free anything, and why file handles and locks get released promptly in practice. A cycle detector handles the case reference counts cannot: objects that refer to each other and so never reach zero.
del name is frequently misread as "delete the object". It does not. It removes the binding — one label comes off. If other labels remain, the object lives on entirely unbothered.
Click Run to execute the code.
This is why the mutable-default-argument bug bites so hard, and it is the same rule wearing a disguise. def add(item, basket=[]) evaluates that empty list once, when the function is defined, and binds it into the function object. Every call that omits the argument shares that one list, so a "fresh" basket accumulates across calls — and in a long-lived web process, across requests and users. The fix is basket=None plus if basket is None: basket = [], which builds a new list per call. Part III returns to this properly; it is listed here because the cause is nothing but "assignment binds, it does not copy".
Common Mistakes
- Reading
b = aas a copy. It attaches a second name to one object. Ifais mutable, you now have two ways to change the same data. - Believing a shallow copy is enough.
list(nested)copies the outer list only; the inner objects are still shared. Usecopy.deepcopywhen the contents are mutable. - Mutating a caller's argument silently. Either document it or copy first. A function that quietly rewrites input it did not create is the hardest kind of bug to trace back.
- Expecting
x = new_valueinside a function to reach the caller. It rebinds a local name. Return the value instead. - Using
isto compare numbers or strings. It appears to work for small values because of CPython's caches, then fails at 257 or on a runtime-built string. Use==for values,isonly forNone,TrueandFalse. - Writing
if x == None. Correct but weaker: a custom__eq__can claim equality withNone.is Nonecannot be fooled. - Assuming
+=andx = x + …are interchangeable. On lists the first mutates in place and the second rebinds — the difference between a function with a side effect and one without. - Thinking
del xfrees memory. It removes one binding. The object survives while any other reference exists. - Using a mutable default argument. The default is created once at definition time and shared by every call that omits it.
- Assuming
id()values are stable or meaningful. They are unique only among live objects; a freed object's id can be reused by the next one. Compare ids, never store them.
Interview Corner
items = [...] inside the function is invisible outside it. But the function can mutate the shared object, so items.append(...) is visible to the caller. That is why the behaviour looks like by-value for integers and strings and like by-reference for lists and dicts — it is neither, it is one rule, and what changes is whether the object can be mutated at all.is and ==, and when identity is the right test.== asks whether two objects are equal, which the objects decide themselves through __eq__; is asks whether two expressions refer to the same object, which the interpreter answers by comparing identity. Equal is the common question, so == is almost always what you want. Identity is right in exactly one family of cases: the singletons — None, True, False — and sentinel objects you create yourself for "no value supplied", where you specifically need a value that cannot be mistaken for a legitimate input. x is None is also immune to a class defining a misleading __eq__, which x == None is not. The trap is that CPython caches small integers from −5 to 256 and interns many string literals, so is returns True for small values and quietly stops doing so at 257 or for strings built at runtime. Code that relies on that passes its tests and breaks on a larger input, which is worse than failing immediately.setdefault — has edited the cache for everyone. Nothing looks like a write at the call site, which is why it survives review. I would confirm it by checking identity: whether what two callers receive is the same object as the cached value. If so, the fix is at the boundary, not the call sites — return a copy, and a deep copy if the config nests, or better, return something immutable so mutation raises instead of succeeding. I would then look for the same shape elsewhere: module-level mutable defaults and any mutable class attribute, which is the identical bug shared across instances rather than across requests. The general principle is that a shared mutable object crossing a trust boundary should be copied or frozen at that boundary, because relying on every future caller's discipline is not a design.Take the starter block below and predict every printed line before you run it — write your predictions down, because a prediction you did not commit to is one you will revise after the fact. Then run it and reconcile any mismatch by asking which line rebound a name and which mutated an object. Next, extend it: add a function that takes a nested dictionary and returns a modified copy without touching the original, and prove it with an identity assertion rather than an equality one. Finally, search a codebase you own for functions that take a list or dict and return None; every one of them either mutates its argument or does nothing, and deciding which you intended is the exercise.
Click Run to execute the code.
Summary
- A name is a label, not a box. Objects live in memory; names refer to them, and one object can carry many names.
- Assignment binds, it never copies.
b = agives you a second label on one object, which is only detectable when the object is mutable. - Rebinding moves a label; mutating changes an object. Every name on a mutated object sees the change; a rebound name affects nobody else.
+=mutates a list in place but rebinds an integer, because integers cannot be changed — the same operator, two different mechanisms.- Argument passing is assignment — call by sharing. A function cannot rebind your name; it can mutate your object.
==compares values,iscompares identity. Use==for everything exceptNone,True,Falseand your own sentinels.- CPython caches small integers (−5 to 256) and interns literals, so
isgives misleadingTrueresults that stop at 257 — an implementation detail, never a guarantee. - A shallow copy duplicates the outer object only. Nested mutable contents stay shared;
copy.deepcopyis the fix. - Reference counting frees objects the moment the last reference goes, with a cycle detector for the references counting cannot resolve.
delremoves a binding, not an object. The object survives while any other name refers to it.
In the next chapter, The Object Model, we go one level down: every value carries a type and an identity, type() and isinstance() answer different questions, and even functions and classes turn out to be objects you can pass around.
I have taught this chapter's material more times than any other in Python, and the pattern never varies: the explanation is easy and the unlearning is hard. Nobody struggles with "a name is a label" as a sentence — it takes ten seconds and everyone nods. What takes weeks is that the box model is not a belief you hold consciously, it is the picture that appears unbidden when you read a line of code, and it keeps appearing after you have agreed it is wrong. So the useful exercise is not re-reading the rule; it is predicting output and being wrong in a place where nothing is at stake. Every aliasing bug I have shipped came from a moment where I did not think about the object model at all, because the line looked ordinary — and the whole point of drilling this early is that "looks ordinary" is exactly the condition under which the old picture takes over.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.