Python deepcopy vs copy: When to Use Each
Learn how Python's copy.copy and copy.deepcopy differ: shallow versus recursive copying, nested object handling, performance trade-offs, and when to use each.
Python's copy module offers two ways to create a copy of an object: copy.copy and copy.deepcopy. Choosing between them requires understanding the core syntax, runtime behavior, and practical implementation patterns shown 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 shallow and deep copying 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 mutable objects it can copy, it creates new copies, including nested containers and custom class instances. With the default implementation, the result is independent of the source.
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.
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.
import copy 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 usually more expensive than shallow copies because they allocate new objects for every mutable element in the object graph. The time and memory cost scale with the total number of objects reachable from the source. For built-in containers, a shallow copy allocates one new container; a deep copy allocates additional containers for every nested mutable value.
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 useful when you plan to add or replace entries at the top level and do not need to isolate nested values. 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 observable result, but copy.copy does less work. 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.
import copy 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 Mutable Class Attributes
A subtle issue arises when a class stores mutable data as a class attribute. Deep copying an instance does not create an instance attribute for that class-level value, so the copied instance can still resolve it to the shared class attribute.
import copy class Cache: data = [] # shared mutable class attribute a = Cache() b = copy.deepcopy(a) b.data.append(1) print(a.data) # [1] because a.data and b.data refer to the class attribute
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.