Python deepcopy vs copy: When to Use Each
python deepcopy vs copy: Understand the difference between Python's copy.copy and copy.deepcopy, including memory behavior, performance, and when to use each for mutab...
python deepcopy vs copy requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The Core Difference Between copy.copy and copy.deepcopy
When you assign one Python object to another variable, you are not creating a copy. Both names refer to the same object. The copy module provides two functions that create actual copies: copy.copy for shallow copies and copy.deepcopy for deep copies. The distinction matters whenever an object contains other objects, such as lists, dictionaries, or custom class instances.
A shallow copy creates a new container object but populates it with references to the same elements as the original. A deep copy recursively copies the object and all objects it references, producing fully independent structures. The choice between python deepcopy vs copy determines whether changes to nested data in the copy affect the original.
What copy.copy Does: Shallow Copy Behavior
copy.copy builds a new object of the same type, then inserts references to the original's elements. For a list of integers, this is indistinguishable from a deep copy because integers are immutable. For a list of lists, the outer list is new, but each inner list is shared.
import copy original = [[1, 2], [3, 4]] shallow = copy.copy(original) shallow[0].append(99) print(original) # [[1, 2, 99], [3, 4]]
The outer list shallow is a different object, but shallow[0] and original[0] point to the same inner list. Mutating that inner list changes both. This is the fundamental behavior of a shallow copy.
What copy.deepcopy Does: Recursive Copying
copy.deepcopy recursively traverses the object graph. For every object it encounters, it creates a new copy, including nested containers and custom objects that support deep copying. The result is a completely independent structure.
import copy original = [[1, 2], [3, 4]] deep = copy.deepcopy(original) deep[0].append(99) print(original) # [[1, 2], [3, 4]]
Here deep[0] is a new list, so modifying it does not affect original. Deep copies guarantee that no mutable object is shared between the copy and the source, unless the object itself is immutable.
How Python Determines What to Copy: copy and deepcopy
The copy module relies on two special methods that classes can implement to control copying behavior. __copy__ defines how a shallow copy is made, and __deepcopy__ defines how a deep copy is made, with a memo dictionary to handle circular references.
class Config: def __init__(self, values): self.values = values def __copy__(self): return Config(self.values) def __deepcopy__(self, memo): new_values = copy.deepcopy(self.values, memo) return Config(new_values)
If a class does not define these methods, copy.copy and copy.deepcopy use default behavior based on the object's type. For built-in containers, the default is usually correct. For custom classes, you may need to implement these methods to avoid copying resources like file handles or network connections, which should not be duplicated.
Performance and Memory Tradeoffs
Deep copies are significantly more expensive than shallow copies because they allocate new objects for every mutable element in the graph. The time and memory cost scale with the total number of objects reachable from the source. Shallow copies only allocate one new container, so they are fast and use little extra memory.
This difference matters in performance-sensitive code. If you only need to modify the top-level structure, a shallow copy is sufficient. If nested objects must be isolated, a deep copy is required, but you should be aware of the cost. For large data structures, consider whether a deep copy is truly necessary or whether an immutable design could avoid the need entirely.
Common Pitfalls with Shallow and Deep Copies
The most frequent mistake is assuming a shallow copy protects nested data. Modifying a nested list, dictionary, or object in a shallow copy will silently change the original. This leads to bugs that are hard to trace because the assignment appears to be a copy.
Another issue is deep copying objects that contain non-copyable resources, such as open file handles, sockets, or locks. deepcopy will attempt to copy them, which may raise an error or produce an invalid object. You can override __deepcopy__ to return the same resource or raise a clear exception.
Circular references are handled by deepcopy through a memo dictionary, so you do not need to worry about infinite recursion. However, the memo also means that if the same object appears multiple times in the structure, it will be copied only once and shared within the deep copy, preserving the original's internal sharing.
Choosing Between copy.copy and copy.deepcopy
Use copy.copy when you need a new top-level container but are comfortable sharing nested mutable objects. This is common when you want to snapshot a configuration dictionary while still allowing nested values to be updated in place. Use copy.deepcopy when the copied object must be fully independent, such as when you are about to mutate nested data without affecting the original.
The decision also depends on the size and depth of the object. For flat structures with only immutable elements, copy.copy and copy.deepcopy produce the same result, but copy.copy is faster. For complex graphs with many nested objects, deepcopy is the only way to achieve isolation, but you must weigh the runtime cost.
Handling Non-Copyable and Custom Objects
Some objects cannot be copied with the default implementation. For example, a class that wraps a database connection should not be deep-copied because the connection is a system resource. In such cases, implement __deepcopy__ to return the same object or to raise an error if copying is not supported.
class Connection: def __deepcopy__(self, memo): raise TypeError("Connection objects cannot be deep-copied")
You can also choose to return a new connection that shares the same underlying resource, but that requires explicit design. The key is to make the copy behavior explicit so that callers know what to expect.
Deepcopy with Custom Classes and Mutable Defaults
A subtle issue arises when a class stores mutable defaults as class attributes. Deep copying an instance will copy the class attribute reference if it is not overridden, which can lead to unexpected sharing. Consider using __slots__ or explicit initialization to avoid this.
class Cache: data = [] # shared mutable class attribute a = Cache() b = copy.deepcopy(a) b.data.append(1) print(a.data) # [1] because class attribute is shared
To avoid this, define data in __init__ so each instance gets its own list. Deep copy will then copy that list independently.
The interaction between class attributes and deepcopy is a common source of confusion. Always test your copy behavior with nested mutable objects to ensure the copy is truly independent.