Object copying is a fundamental concept in Python, and misunderstanding it often leads to hard-to-find bugs. When you modify one object and another one changes unexpectedly, the problem likely lies in how it was copied. Let’s get to the bottom of this once and for all.
🔎 Why Is Object Copying Important in the First Place?
In Python, variables are not “boxes” containing data but rather “labels” or references pointing to objects in memory. When you perform a simple assignment, like this:
list_a = [1, 2, 3]
list_b = list_a
You are not creating a new list. You are simply creating another reference (list_b) that points to the exact same list object.
👉 Any change to list_a will immediately be reflected in list_b (and vice versa) because they are the same object.
Analogy: Imagine you have a Google Doc. If you send a friend a link to it, they will be editing your original document. That’s an assignment. However, if you select “File” → “Make a copy,” a completely independent document is created. The copy module in Python exists to create these kinds of independent copies.
⚙️ Two Types of Copying
The copy module provides two key functions:
copy.copy()— creates a shallow copy.copy.deepcopy()— creates a deep copy.
The main difference between them is how they handle nested objects (like lists within lists or dictionaries within dictionaries).
🟦 Shallow Copy (copy.copy())
copy.copy() creates a new top-level container object but, instead of copying the nested objects, it inserts references to them into the new container.
import copy
original_list = [1, 2, [3, 4]]
shallow_copy = copy.copy(original_list)
# Modify the nested list in the original
original_list[2][0] = 99
print(f"Original: {original_list}") # Output: Original: [1, 2, [99, 4]]
print(f"Shallow Copy: {shallow_copy}") # Output: Shallow Copy: [1, 2, [99, 4]]
As you can see, modifying the nested list affected both the original and the copy because they share that nested list.
ASCII Diagram:
original_list → [ 1, 2, ───► list_inner ]
shallow_copy → [ 1, 2, ───► list_inner ]
list_inner → [ 99, 4 ]
🟩 Deep Copy (copy.deepcopy())
copy.deepcopy() solves the problem of shared references. This function recursively traverses the entire object and creates full, independent copies of everything it encounters, including all nested objects.
import copy
original_list = [1, 2, [3, 4]]
deep_copy = copy.deepcopy(original_list)
# Modify the nested list in the original
original_list[2][0] = 99
print(f"Original: {original_list}") # Output: Original: [1, 2, [99, 4]]
print(f"Deep Copy: {deep_copy}") # Output: Deep Copy: [1, 2, [3, 4]]
Now, the deep_copy is completely independent.
ASCII Diagram:
original_list → [ 1, 2, ───► list_a ]
deep_copy → [ 1, 2, ───► list_b ]
list_a → [ 99, 4 ]
list_b → [ 3, 4 ]
📑 Cheat Sheet: copy.copy() vs copy.deepcopy()
| Object Type | Example | copy.copy() (shallow) | copy.deepcopy() (deep) |
|---|---|---|---|
| list | [1, 2, [3, 4]] | A new list, but the nested [3, 4] is shared | A completely independent copy; the nested list is also copied |
| dict | {"a": 1, "b": {"c": 2}} | A new dict, but the nested {"c": 2} is shared | An independent dictionary and all nested dictionaries |
| set | {1, 2, (3, 4)} | A new set; nested mutable elements would be shared | A new set; all nested elements are recursively copied |
| tuple | (1, 2, [3, 4]) | Returns the same tuple, but the nested list is shared | A new tuple with a copy of the nested list |
| str, int, float, bool | "hi", 42, 3.14, True | Returns the same object (they are immutable, no need to copy) | Same as shallow copy |
| custom class | Node(1, Node(2)) | A new top-level object; nested objects are shared | A completely independent copy of the object and all nested objects |
object with __slots__ | A class with __slots__ | Only the top-level object is copied | All nested objects are copied if they support copying |
| file / socket / stream | open("file.txt") | Error (uncopyable object) | Error (uncopyable object) |
🧪 Live Examples for the Table
📌 list
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # [1, 2, [99, 4]]
print(f"deepcopy: {c}") # [1, 2, [3, 4]]
📌 dict
import copy
a = {"a": 1, "b": {"c": 2}}
b = copy.copy(a)
c = copy.deepcopy(a)
a["b"]["c"] = 42
print(f"copy: {b}") # {'a': 1, 'b': {'c': 42}}
print(f"deepcopy: {c}") # {'a': 1, 'b': {'c': 2}}
📌 set
import copy
a = {1, 2, (3, 4)} # Sets can only contain immutable types
b = copy.copy(a)
c = copy.deepcopy(a)
print(b == c) # True (but they are two independent objects in memory)
📌 tuple
import copy
a = (1, 2, [3, 4]) # The tuple is immutable, but its list element is not
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # (1, 2, [99, 4])
print(f"deepcopy: {c}") # (1, 2, [3, 4])
📌 Custom class
import copy
class Node:
def __init__(self, value, child=None):
self.value = value
self.child = child
a = Node(1, Node(2))
b = copy.copy(a)
c = copy.deepcopy(a)
a.child.value = 99
print(f"copy: {b.child.value}") # 99
print(f"deepcopy: {c.child.value}") # 2
📌 Customizing Behavior: __copy__ and __deepcopy__
You can control how your own objects are copied by defining special methods in their class.
import copy
class Custom:
def __init__(self, x):
self.x = x
def __copy__(self):
print("Called __copy__!")
# Return a new instance, but with the same reference to internal data
return Custom(self.x)
def __deepcopy__(self, memo):
# memo is a dictionary to track already copied objects (prevents recursion)
print("Called __deepcopy__!")
# Create a completely new copy, including nested data
return Custom(copy.deepcopy(self.x, memo))
obj = Custom([1, 2])
c1 = copy.copy(obj) # Output: Called __copy__!
c2 = copy.deepcopy(obj) # Output: Called __deepcopy__!
⚡ Performance and Pitfalls
- Speed:
copy.copy()is significantly faster because it doesn’t recursively traverse the entire data structure.copy.deepcopy()can be very slow for large and deeply nested objects. - Uncopyable Objects: System resources like open files, sockets, or streams cannot be copied. Attempting to do so will raise an error.
- Cyclic References:
deepcopyis smart enough to handle cyclic references (where objectArefers toB, andBrefers back toA) without getting into an infinite loop.
When to Use What: The Main Rules
- Use a shallow copy (
copy.copy()) if your object contains only immutable data or if you intentionally want the copy and the original to share nested objects. It’s fast and efficient. - Use a deep copy (
copy.deepcopy()) when you need a complete, 100% independent copy of the original, especially when working with complex data structures (lists, dictionaries, class instances). - For immutable objects (numbers, strings, tuples without mutable elements), copying is generally pointless—Python optimizes their use anyway.
- Remember you can customize the process with
__copy__and__deepcopy__for full control.