Home Session 01
Foundations · Session 01

Variables & Mutability

x = 42 is not putting a number in a box. It is sticking a nametag on something that already lives in memory. Two nametags can share one thing. We will start there, then get precise about identity, mutation, and references.

~25 minutes
4 interactive demos
Beginner friendly
Learning goal

A variable is a nametag, not a box.

Start there. x does not 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 line by line. One panel holds the Python you wrote; the other holds the nametags and the objects they stick to. Same address means the same thing — two notes, one 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 type, value, 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 manually. 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 is not the number. It is stuck on a real thing sitting in memory. Two notes can sit on the same thing. Moving a note does not 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 objects. Assignment with = binds a name to an object. It does not copy the object unless you explicitly ask Python to make a copy.

What is id()?

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

Every object has a unique identity — in CPython, 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 is mutability. Once a name points at an object, ask: can this object change in place, or must a “change” 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 is 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 are never freed, so sys.getrefcount(42) returns a huge fixed number rather than a count you can watch move. There is 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 refcount reaches 0, CPython immediately frees the object's memory. No waiting, no delay — garbage 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 periodically — not immediately. This is why CPython doesn't only use reference counting.
Mutation vs Rebinding

The most important distinction in Python

In plain words Did you move the nametag, or did you edit the thing it was stuck to? Moving the nametag leaves 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 is 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 do not need to memorize 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. Cannot 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. A singleton — there is exactly one None object in the entire 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)
Unordered collection of unique items. Mutable — you can add/remove. But elements 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 Functions can modify the list you passed — be careful with aliases!
dict {"k": v} Yes A function gets a second name for the same dict — 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 -5 through 256. So a = 5; b = 5 gives a is bTrue: one object, two names. It does the same for short identifier-like strings, which is why s = "hello"; t = "hello" also gives s is tTrue.

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 dFalse — 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 are 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 — never write code whose correctness depends on it.
Rule
The golden rule
Immutable objects are safe to share — no one can change them through another reference. Mutable objects need careful handling when passed around — 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.

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 immediately. The cyclic collector is only needed when objects point at each other.