Home › Session 01
Foundations · Session 01

Variables & Mutability

x = 42 isn't putting a number in a box. It's sticking a nametag on something that already lives in memory, and two nametags can share one thing. We'll start there, then get precise about identity, mutation and references.

~25 minutes
4 interactive demos
Start here, no prerequisites needed
Learning goal

A variable is a nametag, not a box.

Start there. x doesn't hold 42, it points at an object that is 42. Once that feels natural, the technical words (identity, mutation, aliasing, refcount) are just names for things you can already see.

Names vs objects Identity Rebinding Mutation Aliasing Refcount
Recommended path
1 Watch the stage. Step through each line and notice which object appears or changes.
2 Name the operation. Ask: did Python rebind a name, mutate an object, or add another reference?
3 Read the guide. Use the narrative below to connect the animation to everyday Python code.
Interactive lab

Follow one idea at a time

Choose a demo, then move through it line by line. One panel holds the Python you wrote. The other holds the nametags and the objects they're stuck on. If two names show the same address, they're on the same object.

Tip: if a concept feels unclear, pause on that step and compare the highlighted code line with the memory object highlighted alongside it.
1 Choose a demo
›
2 Press Play, Next, or →
›
3 Watch memory update
›
4 Read the explanation below
Namespace: names pointing at objects
Heap: objects with a type, a value and a refcount
→ next ← back Space play R reset
Immutable
Mutable
python
1 / 1
Memory
namespace heap
1

Nothing has run yet

No names, no objects. The namespace is empty and so is the heap. Press Play to autoplay, or use Next to step through by hand, and watch the Memory panel as objects appear on the heap.

What is a variable in Python?

In plain words Imagine a sticky note that says x. The note isn't the number. It's stuck on a real thing sitting in memory. Two notes can sit on the same thing, and moving a note doesn't change the thing. It just points the note somewhere else.

The precise model has three parts: the name (the sticky note), the object (the thing in memory), and the reference (the stickiness connecting them).

1. Name

The label you type, such as x. It lives in a namespace. It is not a storage box.

2. Object

The actual value: 42, "hello", [1, 2, 3]. These live on the heap and each has its own identity.

3. Reference

The connection. Assignment sticks a name onto an object. More than one name can share one object.

Many languages teach a variable as a named box that contains a value. Python is different. When you write x = 42, Python creates or finds an integer object, then binds the name x to it. The name does not contain the number; it points at the object that represents it.

Model
Assignment binds a name
In Python, everything is an object: numbers, strings, functions, classes, all of them. Assignment with = binds a name to an object. It doesn't copy the object unless you ask Python for a copy.

What is id()?

In plain words id() answers one question: are these two names looking at the same thing, or at two things that happen to look alike? The same id means one object with two nametags.

Every object has a unique identity, which in CPython is a stand-in for its memory address. id(x) returns that as an integer. Two names with the same id() point to the exact same object. No copy was made.

python
x = 42 y = x print(id(x) == id(y)) # True, same object print(x is y) # True, 'is' checks identity
Tip
is vs ==
== checks value equality: do the objects look the same?
is checks identity: are they literally the same object in memory?
Use is None, is True, is False for singletons. For everything else, prefer ==.
Mutable or Immutable

Can the object itself change?

In plain words Some things you can edit (a shopping list). Some things you can only replace (a printed number). If you "change" a number, Python throws the old one away and points your name at a new one.

That's mutability. Once a name points at an object, ask whether the object can change in place, or whether a "change" has to create a different object.

Immutable

The object's value cannot change. A "change" creates or finds another object and rebinds the name to it.

int float str tuple
Mutable

The object's contents can change while its identity stays the same. All aliases see the updated contents.

list dict set
Key
Most surprises come from sharing mutable objects
a = [1, 2, 3] followed by b = a does not copy the list. Both names point to one object. If b.append(4) runs, a sees the new item too.
Reference Counting

How Python tracks when to free memory

In plain words Python counts how many nametags are stuck on an object. When you add a name, the count goes up. When you peel one off, it goes down. When the last nametag comes off, Python can throw the object away.

That hidden counter is the reference count. CPython updates it as names and containers are bound and unbound. At zero the object is freed immediately, unless it's stuck in a cycle, which needs a separate collector.

Try this on a list, not on a number. A few values (small integers, True, False, None) are immortal in CPython 3.12 and later. Their count is pinned and they're never freed, so sys.getrefcount(42) returns a huge fixed number rather than a count you can watch move. There's more on that further down.

↑

Refcount increases when a new name is bound to the object (x = obj, b = a, or passing it as a function argument).

↓

Refcount decreases when a name is rebound (x = other), deleted (del x), or goes out of scope (function returns).

✕

When the refcount reaches 0, CPython frees the object's memory right away. No waiting and no delay, it's collected on the spot.

python
import sys x = [1, 2, 3] print(sys.getrefcount(x)) # 2 (x + getrefcount's argument) y = x # y also points to the list print(sys.getrefcount(x)) # 3 del y # y removed; refcount drops print(sys.getrefcount(x)) # 2 again
Warn
Reference cycles
If two objects reference each other (e.g., a list containing itself), their refcounts never reach 0 even when nothing else points to them. CPython has a cyclic garbage collector that handles these cases, but it runs now and then rather than immediately. That's why CPython doesn't only use reference counting.
Mutation vs Rebinding

The distinction that matters most

In plain words Did you move the nametag, or did you edit the thing it was stuck to? Moving the nametag leaves the other nametags alone. Editing the thing is visible to everyone still pointing at it.

Those two operations are rebinding and mutation. Almost every "why did my list change?" bug comes from mixing them up.

Rebinding

x = new_value makes the name point to a different object. The old object is unchanged; it simply loses one reference.

Mutation

x.append() or x[0] = ... changes the existing object. Every name pointing at that object observes the change.

python
# ── REBINDING (immutable types) ───────────────────────────── s = "hello" t = s # both point to the same "hello" object s = "world" # s rebound to a NEW object; t still "hello" print(t) # hello, unaffected # ── MUTATION (mutable types) ───────────────────────────────── a = [1, 2, 3] b = a # both point to the same list a.append(4) # mutates the shared object print(b) # [1, 2, 3, 4], b sees the change

Immutable types (int, str, float, tuple) can only be rebound. You can never change the object itself. Mutable types (list, dict, set) can be both mutated and rebound.

Operation Example Creates new object? id() changes? Other aliases see change?
Rebind x = 99 Yes Yes No
Augmented rebind x += 1 (int) Yes Yes No
Mutate (append) lst.append(x) No No Yes
Mutate (index) lst[0] = x No No Yes
Augmented rebind lst += [x] (list) No* No* Yes*

* For lists, += calls __iadd__ which mutates in-place (unlike with immutable types).

Python's Built-in Types

All the Variable Types

In plain words You don't need to memorise every type today. For each one, ask one question: can I edit this thing, or can I only replace it?

Python's built-in types differ mainly by mutability, and that decides whether a "change" is a mutation or a rebind.

int immutable
x = 42
x = -7
x = 0
Whole numbers, any size. Immutable: x += 1 creates a new int object, doesn't change the old one.
float immutable
x = 3.14
x = 2.0
x = -0.5
Decimal numbers (64-bit IEEE 754). Immutable. Watch out for floating point precision.
str immutable
s = "hello"
s = 'world'
s = """multi
line"""
Sequences of Unicode characters. They can't be changed after creation: s[0] = 'H' raises a TypeError.
bool immutable
done = True
active = False
A subclass of int! True == 1, False == 0. Immutable singleton objects.
NoneType immutable
result = None
The absence of a value. It's a singleton: there is exactly one None object in the whole Python process.
tuple immutable
t = (1, 2, 3)
t = ("a", True)
Ordered, fixed-length sequence. You can't add/remove/change elements after creation. Great for function return values.
list mutable
nums = [1, 2, 3]
nums.append(4)
nums[0] = 99
Ordered, changeable sequence. You can add, remove, and update items in place. The object's identity stays the same.
dict mutable
d = {"a": 1}
d["b"] = 2
del d["a"]
Key-value store. Mutable and ordered (Python 3.7+). Keys must be immutable (hashable).
set mutable
s = {1, 2, 3}
s.add(4)
s.discard(1)
An unordered collection of unique items. Mutable, so you can add and remove, but the elements themselves must be immutable (hashable).

Quick Reference: Mutability at a glance

Type Literal Mutable? Why it matters
int 42 No x += 1 rebinds x to a new object
float 3.14 No Any arithmetic produces a new float
str "hello" No String methods return new strings; you can't change characters in-place
bool True No Only two boolean objects exist; they're singletons
NoneType None No One None object exists; always use is None to check
tuple (1, 2) No Can't add/remove/change; safe to use as dict keys
list [1, 2] Yes A function can modify the list you passed in, so be careful with aliases
dict {"k": v} Yes A function gets a second name for the same dict, so changes made inside it are visible to the caller
set {1, 2} Yes Can grow/shrink; elements must be immutable/hashable
Warn
The aliasing trap
a = [1, 2, 3] then b = a does NOT copy the list. Both a and b point to the same list object. Doing b.append(4) will also affect a! Use b = a.copy() or b = list(a) to get an independent copy.
Note
Some objects are shared, and a few live forever
CPython builds the small integers once, at start-up, and hands out the same object every time, typically for -5 through 256. So a = 5; b = 5 gives a is b → True: one object, two names. It does the same for short identifier-like strings, which is why s = "hello"; t = "hello" also gives s is t → True.

Outside that cached range there is no promise either way. Two large numbers computed separately are usually different objects (n = 999; c = n + 1; d = 1000; c is d → False), but two identical literals written in the same file often still share one object, because the compiler stores the constant once. Don't test interning with id(5) == id(5): that is True for every number, interned or not, since both halves are the same literal.

One more consequence: in CPython 3.12 and later these shared values (small ints, True, False, None) are immortal. They're never freed, and their reference count is pinned at a huge fixed number. That is why sys.getrefcount(42) prints something like 4294967295 instead of a small count, and why the memory panel shows ∞ for 42 rather than a number that ticks up and down. All of this is an implementation detail, not a language guarantee, so never write code whose correctness depends on it.
Rule
The golden rule
Immutable objects are safe to share, because no one can change them through another reference. Mutable objects need care when passed around, because anyone with a reference can change the object.
Try this in a REPL

a = [1, 2]; b = a; print(a is b) then b = a.copy(); print(a is b). First True (alias), then False (new list).

What you should be able to draw

A namespace of names, heap objects with identities, and arrows for references. Rebinding moves an arrow. Mutation changes the object the arrows already point at.

Common mistakes

Four ways this goes wrong

In plain words Every one of these is the same slip: treating a nametag as if it were the thing it's stuck to. When code surprises you, find the object first, then count the names on it.

Expecting b = a to make a copy

It adds a second nametag to the same object. If the object is a list or a dict, a change through either name is a change both names see. When you want a separate object, ask for one: b = a.copy() or b = list(a). (Session 03 shows why even that copies only one level.)

Thinking del x deletes the object

del removes the name. The object loses one reference, and it is freed only if that was the last one. After a = [1, 2]; b = a; del a, the list is alive and well under b. Nothing you write ever destroys an object directly. You only ever let go of it.

Reading += as "edit in place" for every type

On an int or a str, x += 1 builds a new object and moves the name; other names keep the old value. On a list, x += [1] edits the object, and every alias sees it. The same two characters, two different operations. The famous trap: t = ([1], 2) then t[0] += [3] raises TypeError, and the inner list has already grown to [1, 3], because the mutation happened before the tuple refused the assignment.

Testing values with is

a is b asks "same object?", and for numbers and strings the answer depends on how the objects were made. A cached small int says True, two computed 1000s say False, and two literals in one file often say True. None of that is a language promise. Compare values with ==; keep is for None, True and False, and for checking aliasing while you learn.

Check
One question answers most of these
"How many names are on this object right now?" If the answer is more than one, a mutation through any of them is visible through all of them, and a rebinding of one of them changes nothing for the rest.
Check your understanding

Can you predict the memory?

If an answer surprises you, rewind the matching demo above.

1. After x = 42 then y = x, what is true?
Assignment binds a name to an object. y = x copies the reference, not the integer.
2. a = [1, 2] then b = a then b.append(3). What is a?
Mutation changes the object. Every alias sees it. Use b = a.copy() if you need a separate list.
3. When does CPython free an object that is not in a reference cycle?
Reference counting frees the object straight away. The cyclic collector is only needed when objects point at each other.
4. a = [1, 2], b = a, then del a. What happens to the list?
del removes a name and drops one reference. The list still has one name on it, so its count is 1, not 0, and it stays.
5. s = "hi", t = s, s += "!". What is t?
No string object was edited. s += "!" made a new object "hi!" and rebound s; t still wears its nametag on the old "hi".
6. x = [1, 2], y = x, then x = x + [3]. What is y?
x + [3] is a rebinding: a third list appears and only x moves. If the line had been x += [3], the shared list would have been mutated and y would show [1, 2, 3].