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.
= 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.
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 ==.
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?
The object's value cannot change. A "change" creates or finds another object and rebinds the name to it.
int float str tuple
The object's contents can change while its identity stays the same. All aliases see the updated contents.
list dict set
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.
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.
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.
x = new_value makes the name point to a different object.
The old object is unchanged; it simply loses one reference.
x.append() or x[0] = ... changes the existing object.
Every name pointing at that object observes 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).
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.
x = -7
x = 0
x += 1 creates a new int object, doesn't change the old one.x = 2.0
x = -0.5
s = 'world'
s = """multi
line"""
s[0] = 'H' raises a TypeError.active = False
int! True == 1, False == 0. Immutable singleton objects.t = ("a", True)
nums.append(4)
nums[0] = 99
d["b"] = 2
del d["a"]
s.add(4)
s.discard(1)
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 |
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.
-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 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.
a = [1, 2]; b = a; print(a is b) then b = a.copy(); print(a is b).
First True (alias), then False (new list).
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.