Mutable Object Reference Propagation: The Pitfalls of Copying
After completing this topic, you will:
Understand why copying a list using = in Python modifies the original, and be able to distinguish and use shallow and deep copies.
Copying, but the original changes?
original = [1, 2, 3]copy = original
copy.append(4)print(original) # [1, 2, 3, 4] β The original also changes!copy = original is not a copy. It's simply assigning another name to the same list. original and copy are two names pointing to the same list.
In the previous topic (Variables and Memory), we learned that a variable is not a "box that holds a value," but rather an "arrow that points to an object." This is the moment where that concept becomes problematic in practice.
print(id(original)) # 4392957504print(id(copy)) # 4392957504 β The same address!Real Copy: Slicing
original = [1, 2, 3]copy = original[:] # Full slicing = new list
copy.append(4)print(original) # [1, 2, 3] β The original doesn't changeprint(copy) # [1, 2, 3, 4][:] creates a new list by slicing the list from beginning to end. list(original) or original.copy() have the same effect.
Limitations of Shallow Copy
matrix = [[1, 2], [3, 4]]shallow = matrix[:]
shallow[0][0] = 99print(matrix) # [[99, 2], [3, 4]] β The original also changes!Slicing only copies one level deep. matrix[:] creates a new outer list, but the inner lists [1, 2] and [3, 4] share the same objects. Modifying the inner lists will also affect the original.
This is a shallow copy.
Deep Copy: copy.deepcopy
import copy
matrix = [[1, 2], [3, 4]]deep = copy.deepcopy(matrix)
deep[0][0] = 99print(matrix) # [[1, 2], [3, 4]] β The original doesn't changedeepcopy recursively copies all inner objects as well. This creates a completely independent new object.
When to Use Which
| Scenario | Method | Reason |
|---|---|---|
| 1-dimensional list | lst[:] or lst.copy() | Fast and simple |
| 2-dimensional or more nested lists | copy.deepcopy(lst) | Need inner lists to be independent |
| Dictionary | dict.copy() or {**d} | Shallow copy (one level) |
| Nested dictionaries | copy.deepcopy(d) | Need inner dict/list to be independent |
| Cases where copying is unnecessary | Use = as is | Intentionally share the same object |
The Same Applies to Function Arguments
def add_item(lst, item): lst.append(item) return lst
my_list = [1, 2, 3]result = add_item(my_list, 4)
print(my_list) # [1, 2, 3, 4] β The original changesWhen you pass a list to a function, a reference is passed. Modifying the list with .append() inside the function will also change the original. To preserve the original, make a copy inside the function first:
def add_item_safe(lst, item): new_lst = lst[:] new_lst.append(item) return new_lstMutable vs. Immutable Summary
| Type | Mutable/Immutable | Effect on Original After Copying |
|---|---|---|
int, float, str, tuple | Immutable | No effect (a new object is created anyway) |
list, dict, set | Mutable | Original also changes |
Immutable objects do not have this problem because "modifying" a string always creates a new string. You only need to be careful with mutable objects.
Key Takeaway
=is not a copy, but rather assigning another name to the same object. For one-dimensional lists, use[:]or.copy(), and for nested structures, usecopy.deepcopy(). When passing mutable objects to functions, the original can be modified β if this is not intended, make a copy first.