Python: Mutable & Unmutable
During a recent Python project, I dove into one of the most powerful yet often misunderstood aspects of Python: how it handles mutable and immutable objects. This subtle but crucial difference affects everything from basic variable assignments to function arguments and debugging. This blog post walks through what I learned, using clear examples, analogies, and even a few surprises from the advanced tasks.
ID and Type
Every object in Python has a unique identity (its memory address), a type (like int, list, or dict), and a value. You can check these with the id() and type() functions.
a = 42
a = 42
print(id(a), type(a))
>>> 140734626519648 <class 'int'
b = [1, 2, 3]
print(id(b), type(b))
>>> 23423412312 <class 'list'
print(id(a), type(a))
>>> 140734626519648 <class 'int'
b = [1, 2, 3]
print(id(b), type(b))
>>> 23423412312 <class 'list'
Even if two values look the same, Python may or may not give them the same id depending on the object type and internal optimizations (like small integer caching).
Mutable Objects
Mutable objects can be changed after they are created. Common mutable types include:
Lists (list), Dictionaries (dict), Sets (set) and User_defined objects
fruits = ["apple", "banana"]
print(id(fruits))
>>> 139891213902112
fruits.append("mango")
print(fruits)
>>> ['apple', 'banana', 'mango']
print(id(fruits)) same id as before!
Observe how we added to the list, but the id() stayed the same was modified in place.
Immutable Objects
Immutable objects cannot be changed once created. Examples include:
Integers (int), Floats (float), Strings (str), Tuples (tuple)
x = 10
print(id(x))
>>> 140718498804808
x += 1
print(x) # 11
print(id(x))
140718498804840 # different id!
Even though it looks like we "changed" x, Python actually created a new object for 11 and pointed x to it.
Why It Matters
Understanding mutability helps prevent bugs and unexpected behavior. Python treats mutable and immutable types differently in memory. For example:
def modify_list(lst):
lst.append(4)
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list)
>>> [1, 2, 3, 4] # changed!
def modify_int(n):
n += 1
x = 10
modify_int(x)
print(x)
>>> 10 # 10 unchanged!
Lists are mutable and passed by reference, so changes stick. Integers are immutable, so the function’s n is a new object, not the original x.
Function Arguments & Implications
When you pass arguments to functions, Python passes references to the objects, not copies. But what happens depends on whether the object is mutable or immutable.
def change_string(s):
s += " world"
msg = "hello"
change_string(msg)
print(msg)
>>> "hello" # unchanged
def change_dict(d):
d["new"] = True
settings = {"dark_mode": False}
change_dict(settings)
print(settings)
>>> {'dark_mode': False, 'new': True}
This distinction is critical in data science, backend systems, and even simple scripts. Mutating inputs can introduce side effects if not handled carefully.
Advanced Lessons
In advanced tasks, I explored aliasing and deep copies. One key takeaway was the use of copy and deepcopy to avoid unintended mutations.
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0][0] = 99
print(shallow)
print(deep)
>>> [[99, 2], [3, 4]] # shallow copy is affected
>>> [[1, 2], [3, 4]] # deep copy is safe
This was especially important when working with nested structures or function defaults.
Final Thoughts
This journey helped me better appreciate Python’s memory model. Whether you’re new to Python or a seasoned dev, understanding mutability helps write safer, cleaner, and more predictable code. Let me know your thoughts and if you’ve ever been tripped up by a mutable default argument. We’ve all been there!.