Python Shallow Copy: What It Copies and What It Doesn't
Understand python shallow copy: how it works, when to use it, and why nested objects remain shared. Includes practical examples and pitfalls.
When you copy a Python object, you might expect a completely independent duplicate. But python shallow copy creates a new container while keeping references to the same inner objects. This behavior is often misunderstood and leads to subtle bugs when nested data is modified.
What a Shallow Copy Actually Copies
When you create a python shallow copy, Python builds a new container object—such as a list or dictionary—and populates it with references to the same elements as the original. The outer structure is independent, but the inner objects are shared. This means that modifying the copy's structure (adding or removing elements) does not affect the original, but modifying a shared nested object does affect both.
Consider a list of lists:
original = [[1, 2], [3, 4]] shallow = original.copy() shallow.append([5, 6]) # Only the outer list changes shallow[0].append(99) # Shared inner list changes both print(original) # [[1, 2, 99], [3, 4]] print(shallow) # [[1, 2, 99], [3, 4], [5, 6]]
The outer list is a new object, but the inner lists are the same objects in memory.
Creating a Shallow Copy in Python
Python provides several ways to create a shallow copy. The copy module's copy() function is the most general, but many built-in types have their own methods.
import copy # Using copy.copy shallow1 = copy.copy(original) # Using list.copy() for lists shallow2 = original.copy() # Using slicing for lists shallow3 = original[:] # Using dict.copy() for dictionaries original_dict = {"a": [1, 2], "b": [3, 4]} shallow_dict = original_dict.copy()
For custom classes, copy.copy() works if the object is picklable or defines __copy__. If not, it may fall back to __reduce_ex__, which can be slow or fail for objects with complex state.
The Shared Reference Problem
The most common bug with shallow copies is assuming that all nested data is independent. When you mutate an object that appears in both the original and the copy, the change propagates to both. This often surfaces when copying a list of dictionaries or a list of custom objects.
users = [{"name": "Alice", "roles": ["admin"]}, {"name": "Bob", "roles": ["user"]}] users_copy = users.copy() users_copy[0]["roles"].append("editor") print(users[0]["roles"]) # ['admin', 'editor'] print(users_copy[0]["roles"]) # ['admin', 'editor']
The dictionary itself is shared because the shallow copy only copies the reference to the dictionary, not its contents.
Shallow Copy vs Deep Copy
The alternative to a shallow copy is a deep copy, which recursively copies all nested objects. The copy.deepcopy() function creates a fully independent duplicate.
| Aspect | Shallow Copy | Deep Copy |
|---|---|---|
| Outer container | New object | New object |
| Nested objects | Shared references | Recursively copied |
| Memory usage | Low (only new outer structure) | Higher (duplicates all objects) |
| Speed | Fast (no recursion) | Slower (recursive traversal) |
| Use case | When nested objects are read-only | When full independence is required |
Choosing between them depends on whether you need to isolate changes to nested data. If the nested objects are immutable (like tuples or strings), a shallow copy behaves like a deep copy in practice because immutable objects cannot be modified.
Performance and Memory Tradeoffs
Shallow copies are cheap because they avoid recursion and object duplication. Creating a shallow copy of a large list is O(n) in the number of elements, but each element is just a reference. Deep copies are O(total size of the object graph) and can be significantly slower and more memory-intensive.
This matters in performance-sensitive code where you only need to protect the outer structure. For example, if you are passing a list to a function that might reassign the list but never mutate its elements, a shallow copy is sufficient and avoids the cost of deep copying.
Common Pitfalls and How to Avoid Them
One subtle trap is assuming that copy.copy() on an immutable object creates a new object. In CPython, copy.copy() returns the same object for immutable types like tuples and frozensets because there is no need to copy them. This is an optimization but can surprise developers who expect a distinct object.
Another pitfall is using shallow copy on a custom class without understanding its internal references. If your object contains a mutable attribute, that attribute remains shared. If you need full isolation, either implement __deepcopy__ or use copy.deepcopy().
When you need a shallow copy for a specific type, prefer the built-in methods (list.copy(), dict.copy()) over copy.copy() because they are faster and more explicit. For nested data that must be independent, use copy.deepcopy() and be aware of its performance cost.