Names, Objects & Mutability

The one mental model that fixes 90% of Python surprises for a Java developer: a variable is a name (a sticky label), not a box that holds a value. Assignment never copies — it just moves a label.

names vs objectsmutable vs immutable aliasingis vs ==exam Q5

Why this matters first

Coming from Java, you picture a variable as a typed box: int x reserves a slot that holds the number 5; an object variable holds a reference to an object. Python looks similar but the model is subtly — and importantly — different. In Python there are no boxes. There are objects floating in memory (every value is an object: a list, an int, a string, a function), and there are names that are stuck onto them like sticky labels. = doesn't put a value into a variable; it binds a name to an object.

This single shift explains the behaviour that trips up every newcomer: why two variables sometimes change together, why += on a list behaves differently than on a number, and why a function can quietly mutate the list you passed in. Get this model right once and those stop being surprises.

The rule, in one line: assignment binds a name to an object. It never copies the object, and it never recurses into the object. Everything below follows from that.

Mutable vs immutable — the other half

Objects come in two flavours. Mutable objects can be changed in place after they're created: list, dict, set, and most custom classes. You can append to a list and it's still the same object, just with new contents. Immutable objects can never change once created: int, float, str, tuple, bool, frozenset. When you "change" an immutable value, Python actually builds a new object and rebinds the name to it — the original is untouched.

That's why x = 5; y = x; y += 1 leaves x at 5 (ints are immutable, so += made a new 6 and rebound y), but the same pattern on a list lets the change leak across names. The interactive below walks exactly that scenario for a list so you can watch the labels move.

Step through the labels — what each line really does

How to read it: the boxes on the left are names (a, b); the boxes on the right are objects in memory (with a pretend address like 0x1a). An arrow means "this name is currently stuck on this object." Watch how b = a makes two arrows hit the same box (aliasing), how .append changes a box's contents without moving any arrow, and how re-assignment swings an arrow to a new box.

The big payoff: is vs ==

Because names and objects are separate, Python gives you two different questions. == asks "do these two objects have the same value?" — it compares contents. is asks "are these the very same object?" — it compares identity (literally whether the arrows point at the same box). The built-in id(x) returns that object's identity, so a is b is just id(a) == id(b). Two lists [1,2] == [1,2] is True (same contents) but [1,2] is [1,2] is False (two separate boxes). The everyday rule: use == for values, and reserve is for the singletons None, True, False (write if x is None:).

Functions don't copy either — "pass by object reference"

When you call f(my_list), Python binds the parameter name inside the function to the same object you passed — no copy is made. So if the function does my_list.append(…) (a mutation), your original list changes. But if the function does my_list = […] (a rebind), it only moves the function's local label; your variable outside is unaffected. This is neither Java's "pass by value" nor "pass by reference" exactly — it's often called pass by object reference (or "call by sharing"). The mutate-vs-rebind distinction you just watched is the whole story.

The classic trap: a mutable default argument

Here's where the model earns its keep. A function's default values are evaluated once, when the def runs — not on every call. So a mutable default (like =[]) is created a single time and shared across every call. Click below and watch the buggy version accumulate state between calls, while the idiomatic =None fix stays clean.

def add(x, bag=[]):
    bag.append(x)
    return bag

Buggy — one list, made at def time, reused forever:

def add(x, bag=None):
    if bag is None: bag=[]
    bag.append(x)
    return bag

Fixed — a fresh list each call:

calls: 0

Strengths, gotchas & the rules of thumb

The names-and-objects model is what makes Python fast to write and flexible — no manual copying, cheap passing of big objects, easy sharing. The cost is exactly the surprises above: shared mutable state. Internalise these and you'll rarely get bitten:

Exam Q5 connection: the question "which of these is mutable?" is really asking which objects can change in placelist, dict, set can; str, tuple, int, frozenset cannot. The deeper reason matters more than the answer: mutability + shared references is what produces every aliasing bug you'll meet.