
When I started learning Python, I kept running into questions like:
- Why does changing a list inside a function affect the original?
- Why does
a is bsometimes returnTrueand sometimesFalseeven when the values are the same? - Why does
a += [4]behave differently froma = a + [4]?
These aren't random quirks — they all come down to one fundamental idea: in Python, everything is an object. Once you understand that, a lot of confusing behavior starts making sense.
id() and type() — Every Object Has an Identity and a Type
In Python, every object has two things:
-
type()— what kind of object it is -
id()— where it lives in memory (its unique identity)
a = 89
print(type(a)) # <class 'int'>
print(id(a)) # 140234567891234 ← memory address
Think of id() like a home address — two people can have the same name (value) but live at different addresses (identity).
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True ← same values
print(a is b) # False ← different objects in memory
This is the difference between == (same value?) and is (same object?).
Mutable Objects — Things That Can Change
A mutable object can be changed after it's created — without creating a new object. Lists, dictionaries, and sets are mutable.
a = [1, 2, 3]
print(id(a)) # 139926795932424
a.append(4)
print(id(a)) # 139926795932424 ← same address!
print(a) # [1, 2, 3, 4]
The list changed — but the object is still the same one in memory. This is what "mutable" means: the object itself can be modified in place.
The += vs = + Trick
This is where mutability gets interesting. Consider this:
a = [1, 2, 3]
a += [4] # modifies in place → same address
print(id(a)) # same!
a = a + [4] # creates a NEW list → new address
print(id(a)) # different!
a += [4] calls a.__iadd__([4]) — the i stands for in-place. It extends the existing list without creating a new one.
a = a + [4] creates a brand new list, then assigns it to a. The old list is gone.
This matters a lot when multiple variables point to the same list:
a = [1, 2, 3]
b = a # b points to the SAME list
a += [4] # modifies in place
print(b) # [1, 2, 3, 4] ← b sees the change!
a = a + [4] # creates a new list
print(b) # [1, 2, 3, 4] ← b doesn't see this change
Immutable Objects — Things That Cannot Change
An immutable object cannot be changed after it's created. Integers, strings, floats, and tuples are immutable.
a = 89
print(id(a)) # some address
a = a + 1 # creates a NEW integer object
print(id(a)) # different address!
When you "change" an integer, Python actually creates a brand new integer and points your variable to it. The original object is untouched.
A Confusing but Important Special Case
You might think (1) is a tuple — but it's not:
type((1)) # <class 'int'> ← just grouping, like in math
type((1,)) # <class 'tuple'> ← the comma makes it a tuple
type(()) # <class 'tuple'> ← empty tuple, special case
The comma is what makes a tuple, not the parentheses. And () is a special case — there's nothing else empty parentheses could mean, so Python treats it as an empty tuple.
Why Two Tuples with the Same Values Are Different Objects
a = (1, 2)
b = (1, 2)
print(a == b) # True ← same values
print(a is b) # False ← different objects in memory
Even though they look the same, Python creates two separate tuple objects. They live at different addresses.
But with small integers, Python does something clever:
a = 1
b = 1
print(a is b) # True ← Python caches small integers!
Python caches integers from -5 to 256 and reuses the same objects — so a and b actually point to the same object. This is a performance optimization called integer interning.
Why Does It Matter? How Python Treats Them Differently
The key difference is what happens in memory:
Mutable (list, dict, set):
a = [1, 2, 3] → object at address 0x100
a.append(4) → SAME object at 0x100, now [1, 2, 3, 4]
Immutable (int, str, tuple):
a = 89 → object at address 0x200
a = a + 1 → NEW object at 0x300, value is 90
old object at 0x200 still exists (until garbage collected)
This affects how you write code — especially when multiple variables point to the same object:
# mutable — shared reference
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] ← a changed too!
# immutable — no shared state issue
a = 89
b = a
b = b + 1
print(a) # 89 ← a is unchanged
How Arguments Are Passed to Functions
This is where everything comes together. Python passes arguments by object reference — sometimes called "pass by assignment".
What this means:
- the function receives a reference to the same object
- whether it can modify the original depends on whether the object is mutable or immutable
Immutable — function cannot change the original:
def add_one(n):
n = n + 1 # creates a NEW integer, n now points to it
print(n) # 90
a = 89
add_one(a)
print(a) # 89 ← unchanged! function got its own copy
Inside the function, n starts pointing to the same object as a. But when you do n = n + 1, a new integer is created and n now points to that. The original a is untouched.
Mutable — function CAN change the original:
def add_item(my_list):
my_list.append(4) # modifies the SAME list object
a = [1, 2, 3]
add_item(a)
print(a) # [1, 2, 3, 4] ← changed!
The function receives a reference to the same list. When it calls .append(), it modifies that same object — so the change is visible outside the function too.
The Gotcha — Reassigning Inside a Function:
def replace_list(my_list):
my_list = [10, 20, 30] # creates NEW list, local variable only
print(my_list) # [10, 20, 30]
a = [1, 2, 3]
replace_list(a)
print(a) # [1, 2, 3] ← unchanged!
Reassigning my_list inside the function just makes the local variable point to a new list. The original a still points to the old list.
Summary
| Mutable | Immutable | |
|---|---|---|
| Examples | list, dict, set | int, str, tuple |
| Can change in place? | ✅ Yes | ❌ No |
+= behavior |
modifies same object | creates new object |
| Same address after change? | ✅ Yes | ❌ No |
| Function can modify original? | ✅ Yes | ❌ No |
The golden rules:
-
==checks if values are equal -
ischecks if they are the literally same object in memory - mutable objects can be changed in place — watch out for shared references
- immutable objects always create new objects when "changed"
- functions can modify mutable arguments but not immutable ones
Once these click, a huge chunk of Python's behavior that seemed random starts making complete sense.
Top comments (0)