Python Object Copy: Shallow vs Deep Explained
Learn how to copy objects in Python correctly. Understand the difference between shallow and deep copy, when to use each, and how to customize copy behavior with `__copy__` and `__deepcopy__`.
Many Python developers assume that assigning one variable to another creates a copy of the object. In reality, assignment only copies the reference. Understanding object copying in Python is essential to avoid subtle bugs when working with mutable data structures.
Assignment vs Copy: The Reference Problem
When you write b = a, Python does not create a new object. Both a and b point to the same object in memory. For immutable types such as integers and strings, this alias is harmless because the object cannot be mutated. But for mutable objects such as lists, dictionaries, and custom class instances, modifying one variable affects the other.
a = [1, 2, 3] b = a b.append(4) print(a) # [1, 2, 3, 4]
This behavior is often unexpected for developers coming from languages where assignment copies values. To actually duplicate an object, you need an explicit copy operation. Python provides the copy module for this purpose.
Shallow Copy with copy.copy()
The copy.copy() function creates a shallow copy of an object. A shallow copy constructs a new container object and then populates it with references to the elements found in the original. This means the top-level object is new, but nested objects are shared.
import copy a = [[1, 2], [3, 4]] b = copy.copy(a) b[0].append(99) print(a) # [[1, 2, 99], [3, 4]] print(b) # [[1, 2, 99], [3, 4]]
Here, b is a new list, but its inner lists are the same objects as in a. Modifying a nested list through b also changes a. This is the core limitation of a shallow copy.
Shallow copies are appropriate when you only need to protect the top-level structure, or when the object contains only immutable elements.
Deep Copy with copy.deepcopy()
A deep copy creates a new object and recursively copies all objects found within it. The result is fully independent of the original, including nested containers and custom objects.
import copy a = [[1, 2], [3, 4]] b = copy.deepcopy(a) b[0].append(99) print(a) # [[1, 2], [3, 4]] print(b) # [[1, 2, 99], [3, 4]]
Deep copies eliminate shared references entirely. They are the safest choice when you need to modify a copy without any side effects on the original. However, deep copying is more expensive because it traverses the entire object graph.
Customizing Copy Behavior with copy and deepcopy
For user-defined classes, you can control how copies are made by defining the __copy__ and __deepcopy__ methods. These methods are called by copy.copy() and copy.deepcopy() respectively.
import copy class Config: def __init__(self, values): self.values = values def __copy__(self): # Return a new Config with a new top-level list but shared nested values. return Config(list(self.values)) def __deepcopy__(self, memo): # Create the copy before recursing so memo can break cycles. new = Config.__new__(Config) memo[id(self)] = new new.values = copy.deepcopy(self.values, memo) return new
The memo parameter in __deepcopy__ is a dictionary that tracks already-copied objects. If your object can contain a reference to itself, register the new object in memo before copying its attributes, and always pass memo through when copying nested attributes.
Customizing copy behavior is useful when your class contains resources that should not be duplicated, such as file handles or network connections. You can choose to share them or recreate them in the copy.
Copying Nested Data Structures: Common Pitfalls
One frequent mistake is using a shallow copy when a deep copy is required, especially with dictionaries that contain lists or other dictionaries.
import copy original = {"items": [1, 2, 3]} shallow = copy.copy(original) shallow["items"].append(4) print(original["items"]) # [1, 2, 3, 4]
Another pitfall is copying objects that contain references to themselves. copy.deepcopy() handles cycles correctly, but a naive recursive copy would cause infinite recursion. The memo dictionary in __deepcopy__ is exactly for this purpose.
Also be aware that copy.copy() and copy.deepcopy() work with most built-in container types and many custom objects. For custom classes, if you do not define __copy__ or __deepcopy__, the default behavior is based on the object's pickling protocol (__reduce_ex__): Python reconstructs the object and restores its state. This works for many classes, but it can be wrong or fail for objects that hold resources such as file handles or locks. Defining these methods explicitly gives you full control.
Performance and Memory Considerations
Deep copying is significantly slower and uses more memory than shallow copying because it duplicates every nested object. For large data structures, this can become a bottleneck. Shallow copies are inexpensive but leave shared references.
The choice between shallow and deep copy should be driven by how the copy will be used. If you only need to replace elements at the top level, a shallow copy suffices. If you need to modify nested containers in place without affecting the original, a deep copy is required.
For performance-sensitive code, consider whether you can avoid copying altogether by using immutable data structures or by restructuring the logic to not require copies. When copying is unavoidable, profile the operation to understand its cost.
Choosing Between Shallow and Deep Copy
Use a shallow copy when:
- The object contains only immutable elements.
- You intend to replace top-level items without modifying nested ones.
- You explicitly want to share nested objects for performance reasons.
Use a deep copy when:
- The object contains mutable nested structures that will be modified.
- You need a fully independent snapshot of the object.
- You are working with complex objects that may contain cycles.
A practical approach for ordinary containers is to start with a shallow copy and move to a deep copy when you need to modify nested data without changing the original. This keeps the code efficient while avoiding unintended shared references.
Remember that copy.copy() and copy.deepcopy() are not limited to built-in containers. They also work with many custom objects, especially when the class implements __copy__ and __deepcopy__. For custom classes, implementing these methods gives you precise control over what gets duplicated and what gets shared.