In Python, mutability refers to whether an object’s internal state (value) can be changed after it is created.
Python objects fall into two broad categories:
- Mutable objects → Can be changed in place
- Immutable objects → Cannot be changed after creation
Understanding mutability is critical for:
- Debugging unexpected behavior
- Writing safe functions
- Avoiding side effects
- Mastering references and memory
| Category | Types |
|---|---|
| Mutable | list, dict, set, bytearray |
| Immutable | int, float, str, tuple, frozenset, bool |
Variables do NOT store values — they store references to objects.
- Mutable objects → Same object, same memory address
- Immutable objects → New object created on modification
my_list = [1, 2, 3]
print(id(my_list)) # Memory address before modification
my_list.append(4)
print(my_list)
print(id(my_list)) # Memory address remains the sameOutput:
[1, 2, 3, 4]
The list is modified in place
No new object is created
my_str = "Hello"
print(id(my_str))
my_str += " World"
print(my_str)
print(id(my_str))Output:
Hello World
A new string object is created
Original string remains unchanged
my_list = [10, 20, 30]
my_list[1] = 25
print(my_list)Output:
[10, 25, 30]
my_dict = {"name": "Alice", "age": 25}
my_dict["age"] = 26
my_dict["city"] = "New York"
print(my_dict)Output:
{'name': 'Alice', 'age': 26, 'city': 'New York'}
my_set = {1, 2, 3}
my_set.add(4)
my_set.remove(2)
print(my_set)Output (order may vary):
{1, 3, 4}
my_tuple = (1, 2, 3)
# my_tuple[0] = 10 # ❌ TypeErrormy_tuple = ([1, 2, 3], 4)
my_tuple[0][1] = 10
print(my_tuple)Output:
([1, 10, 3], 4)
Tuple is immutable
The list inside it is mutable
my_str = "hello"
new_str = my_str.replace("h", "y")
print(new_str)
print(my_str)Output:
yello
hello
x = 10
print(id(x))
x += 1
print(id(x))
print(x)Output:
11
New integer object is created
b = True
print(id(b))
b = False
print(id(b))True and False are separate immutable objects
my_frozenset = frozenset([1, 2, 3])
# my_frozenset.add(4) # ❌ AttributeError- Copies outer object
- Inner objects are shared
- Copies everything recursively
- No shared references
import copy
original_list = [[1, 2], [3, 4]]
# Shallow copy
shallow_copy = copy.copy(original_list)
shallow_copy[0][1] = 10
print(original_list)Output:
[[1, 10], [3, 4]]
deep_copy = copy.deepcopy(original_list)
deep_copy[0][1] = 5
print(original_list)Output:
[[1, 10], [3, 4]]
Deep copy prevents side effects
| Pitfall | Why It Happens |
|---|---|
| Unexpected list changes | Shared references |
| Function modifying arguments | Mutable parameters |
| Shallow copy bugs | Nested objects |
| Assuming tuple immutability is deep | Mutable elements inside |
| Using mutable default args | Very dangerous |
✅ Use immutable types when possible
✅ Use deepcopy for nested structures
✅ Avoid mutable default arguments
✅ Understand reference sharing
❌ Don’t assume assignment creates copies
| Concept | Key Idea |
|---|---|
| Mutable | Changeable in place |
| Immutable | New object created |
id() |
Memory identity |
| Reference | Variables point to objects |
| Shallow copy | Shared inner objects |
| Deep copy | Fully independent |
- Demonstrate how modifying a list inside a function affects the original list.
- Show how shallow copy behaves with nested dictionaries.
- Explain why tuples can “appear mutable”.
- Fix a bug caused by shared mutable references.
- Convert a mutable design into an immutable one.