Python copy.copy: Shallow Copy Basics and Usage
Learn how python copy.copy creates shallow copies, when to use it, and how it differs from deepcopy in practical scenarios.
When you assign one Python object to another variable, you are not duplicating the object. You are creating a new reference to the same underlying data. This is fine for immutable types, but for mutable objects like lists and dictionaries, it often leads to unintended side effects. The copy module provides copy.copy to create a shallow copy—a new object that shares the inner objects with the original. This article explains how python copy.copy behaves, where it fits, and when you should reach for copy.deepcopy instead.
How copy.copy Works
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. The elements themselves are not duplicated; they are shared between the original and the copy. This is different from a deep copy, which recursively copies all nested objects.
Under the hood, copy.copy checks whether the object's class defines a __copy__ method. If it does, that method is called. Otherwise, it falls back to a default behavior that uses the object's type and __reduce_ex__ to reconstruct a copy. Most built-in mutable types, such as list, dict, and set, support shallow copying directly.
import copy original_list = [1, [2, 3], 4] shallow_copy = copy.copy(original_list) print(shallow_copy == original_list) # True print(shallow_copy is original_list) # False print(shallow_copy[1] is original_list[1]) # True
The nested list [2, 3] is shared. Modifying it through the copy affects the original, and vice versa. This is the core behavior of a shallow copy.
Shallow vs. Deep Copy
The distinction between shallow and deep copy matters when an object contains nested mutable structures. A shallow copy duplicates only the top-level container; a deep copy duplicates everything recursively.
| Aspect | copy.copy (shallow) | copy.deepcopy (deep) |
|---|---|---|
| Top-level container | New object | New object |
| Nested objects | Shared references | Recursively duplicated |
| Memory usage | Lower | Higher |
| Speed | Faster | Slower |
| Use case | Independent outer structure | Fully independent structure |
Consider a dictionary with a list value. A shallow copy gives you a new dictionary, but the list inside is still shared. A deep copy gives you a new dictionary and a new list.
original = {"items": [1, 2, 3]} shallow = copy.copy(original) deep = copy.deepcopy(original) shallow["items"].append(4) print(original["items"]) # [1, 2, 3, 4] deep["items"].append(5) print(original["items"]) # [1, 2, 3, 4]
The shallow copy shares the list, so appending through shallow changes the original. The deep copy is fully independent.
Practical Examples with Common Types
Lists and Dictionaries
For simple lists of immutable elements, a shallow copy is often sufficient. If you need to modify the outer list without affecting the original, copy.copy works.
original = [1, 2, 3] copy_list = copy.copy(original) copy_list.append(4) print(original) # [1, 2, 3] print(copy_list) # [1, 2, 3, 4]
For dictionaries, the same principle applies. The keys and values are references. If a value is mutable, it remains shared.
original = {"a": [1]} copy_dict = copy.copy(original) copy_dict["a"].append(2) print(original) # {'a': [1, 2]}
Custom Objects
For instances of user-defined classes, copy.copy creates a new object and copies the __dict__ (or __slots__) as a shallow copy. This means attributes that are mutable are shared.
class Config: def __init__(self, values): self.values = values original = Config([1, 2]) copy_config = copy.copy(original) copy_config.values.append(3) print(original.values) # [1, 2, 3]
If you need the new object to have an independent values list, you must either use copy.deepcopy or implement __copy__ to control the behavior.
Performance and Memory Considerations
Shallow copies are cheaper than deep copies because they avoid recursive traversal. The time and memory cost of copy.copy is proportional to the number of top-level elements, not the total size of the object graph. For large nested structures, this difference can be significant.
Deep copies require walking the entire object graph, which is O(n) where n is the number of objects reachable. It also creates new objects for every mutable container, increasing memory usage. In performance-sensitive code, avoid deep copies unless you genuinely need full independence.
A common pattern is to use a shallow copy to protect the outer container while accepting that inner objects are shared. This is often sufficient for data transfer objects where the nested data is treated as read-only.
Common Pitfalls and Edge Cases
Unintended Sharing
The most frequent mistake is assuming copy.copy gives you a fully independent object. When nested mutable structures exist, modifications through the copy leak back to the original. This is especially dangerous when passing objects to functions that mutate arguments.
def add_item(data): data["items"].append("new") original = {"items": []} shallow = copy.copy(original) add_item(shallow) print(original["items"]) # ['new']
Objects That Cannot Be Shallow Copied
Some objects, such as file handles, sockets, or generators, cannot be meaningfully copied. The copy module may raise TypeError or return a reference to the same object. Always test custom types to ensure the copy behavior matches your expectations.
Cyclic References
copy.copy handles cyclic references without infinite recursion because it does not traverse nested objects. copy.deepcopy also handles cycles by maintaining a memo dictionary, but it is more complex and slower.
Customizing Copy Behavior with copy
If you need fine-grained control over how your class is copied, define a __copy__ method. This method should return a new instance with the desired attribute values. It is called by copy.copy instead of the default behavior.
class SafeConfig: def __init__(self, values): self.values = list(values) # ensure a fresh list def __copy__(self): return SafeConfig(self.values) # creates a new list
Now copy.copy will produce an independent object because the constructor copies the list. This is useful when you want shallow copy semantics for the outer object but deep copy semantics for specific attributes.
When to Use copy.copy vs deepcopy
Use copy.copy when you need a new top-level container but are comfortable sharing the nested objects. This is typical when:
- The nested objects are immutable (e.g., strings, tuples, numbers).
- You only intend to replace elements at the top level, not modify nested ones.
- You are working with large structures and want to avoid the cost of deep copying.
Use copy.deepcopy when you need complete independence from the original, especially if nested objects are mutable and will be modified. This is common when:
- You are building a snapshot of a configuration object.
- You need to pass an object to code that might mutate it, and you want to protect the original.
- The object graph contains mutable containers that will be altered.
A practical rule: if you are not sure whether nested objects will be mutated, prefer a deep copy. The performance cost is usually acceptable unless the structure is extremely large or the copy happens in a hot loop.
Handling Copy for Objects with slots
Classes that use __slots__ to reduce memory footprint do not have a __dict__. The default shallow copy behavior still works because copy.copy uses __reduce_ex__ to reconstruct the object. However, if you need custom behavior, you must implement __copy__ and manually copy each slot.
class Point: __slots__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y def __copy__(self): return Point(self.x, self.y)
Without __copy__, copy.copy will create a new instance and copy the slot values as references. For immutable values like integers, this is fine. For mutable slot values, you would need to copy them explicitly.
Conclusion
python copy.copy is a lightweight tool for creating shallow copies. It is fast and memory-efficient, but it shares nested mutable objects. Understanding when this sharing is acceptable—and when it is not—is the key to using it correctly. For full independence, copy.deepcopy is the appropriate choice, though it comes with higher overhead. By knowing the mechanics and limitations of copy.copy, you can make deliberate decisions about object copying in your Python code.