Back to Blog
Python

Python Deep Copy: When and How to Use It

python deep copy: Understand Python deep copy: how it differs from shallow copy, when to use it, how to customize it, and common pitfalls.

copy moduleshallow copydeepcopyobject copyingmutable objects
Illustration of a Python object being duplicated into an independent deep copy, with nested containers separated.

python deep copy requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you assign a mutable object to a new variable in Python, you are not creating a copy. You are creating a new reference to the same object. Modifying one variable changes the other. For many tasks, that is fine, but when you need an independent object with its own nested data, you need a deep copy. The copy module provides copy.deepcopy() for exactly this purpose, and understanding how it works is essential for writing correct code that manipulates complex data structures.

What Deep Copy Actually Does

A deep copy constructs a new compound object and then recursively inserts copies of the objects found in the original. This means that not only is the top-level container independent, but every nested object is also independent. If you modify a nested list inside the copied structure, the original remains unchanged. This is different from a shallow copy, which creates a new container but populates it with references to the same elements as the original.

Consider a list of lists:

original = [[1, 2], [3, 4]] shallow = list(original) # shallow copy deep = copy.deepcopy(original)

shallow is a new list, but shallow[0] points to the same inner list as original[0]. deep[0] is a new list. Changing shallow[0].append(99) will affect original, but changing deep[0].append(99) will not.

Shallow Copy vs Deep Copy: A Practical Comparison

The distinction becomes critical when you work with nested mutable structures. The table below summarizes the behavior for a typical nested list:

OperationShallow copyDeep copy
Top-level listNew objectNew object
Nested listsShared referencesNew objects
Modifying nested elementAffects originalDoes not affect original
Modifying top-level elementDoes not affect originalDoes not affect original

Use a shallow copy when you only need to protect the top-level structure and you intend to share nested objects. Use a deep copy when you need full independence, such as when you are about to mutate the nested data and want to preserve the original state.

Using copy.deepcopy() on Common Data Structures

copy.deepcopy() works on most built-in types, including lists, dictionaries, sets, and custom objects. It handles nested combinations of these types automatically. For example:

import copy original_dict = { "users": [{"name": "Alice", "scores": [90, 85]}, {"name": "Bob", "scores": [78, 92]}], "meta": {"version": 1} } copied_dict = copy.deepcopy(original_dict)

Now copied_dict["users"][0]["scores"] is a distinct list from the original. You can safely modify it without affecting original_dict. This is especially useful when you need to pass a snapshot of a configuration or a data structure to another component that may mutate it.

Deep copy also works with custom classes by default, because copy.deepcopy() inspects the object's __dict__ and recursively copies its attributes. However, for classes that contain non-copyable resources, you need to customize the behavior.

Customizing Deep Copy with deepcopy

When a class manages resources that should not be duplicated, such as an open file handle, a network connection, or a lock, the default deep copy behavior is inappropriate. You can define the __deepcopy__ method to control what gets copied and what gets shared or recreated.

import copy class Connection: def __init__(self, endpoint): self.endpoint = endpoint self.socket = open_socket(endpoint) # assume this returns a socket def __deepcopy__(self, memo): # Create a new instance without copying the socket new_conn = Connection.__new__(Connection) new_conn.endpoint = self.endpoint new_conn.socket = open_socket(self.endpoint) # open a new socket return new_conn

The memo parameter is a dictionary that tracks already-copied objects to handle cycles. When you implement __deepcopy__, you must use memo correctly if your object contains references to other objects that might be part of a cycle. A common pattern is to check memo for the object id and store the new copy there.

If you do not need to customize the copy logic, you can rely on the default behavior. But for classes that wrap external resources, a custom __deepcopy__ prevents resource duplication and maintains the correct lifecycle.

Performance and Memory Considerations

Deep copy is significantly more expensive than shallow copy because it recursively traverses the entire object graph. For large data structures, this can consume a lot of memory and CPU time. The cost grows with the number of objects and the depth of nesting. If you only need to protect the outer container, a shallow copy is much cheaper.

Another performance concern is that copy.deepcopy() uses memoization to handle cycles and repeated references. This means it may not duplicate an object that appears twice in the original; it will reuse the same copied object in the copy. This is correct behavior, but it means that the copy is not always a full independent clone if there are shared references in the original. For example, if two keys in a dictionary point to the same list, the deep copy will have both keys pointing to the same new list, preserving the shared structure.

For performance-sensitive code, consider whether you actually need a deep copy or whether you can avoid copying altogether by using immutable data structures or by designing your code to not mutate shared state.

Common Pitfalls and Edge Cases

Deep copy is not a magic bullet. There are several situations where it can fail or produce surprising results.

Recursive and Cyclic Objects

If an object contains a reference to itself, copy.deepcopy() handles it correctly by using the memo dictionary. The copy will also contain a self-reference, but it will point to the new copy, not the original. This is usually what you want, but it can lead to infinite loops if you try to manually copy such structures without memoization.

Non-Copyable Objects

Some objects cannot be deep copied because they are not picklable or because they hold system resources. For example, a threading.Lock or a socket.socket cannot be copied meaningfully. If you attempt to deep copy an object that contains one of these, you will get a TypeError or an AttributeError. The solution is to implement __deepcopy__ to handle those attributes appropriately, as shown earlier.

Objects with slots

Classes that define __slots__ do not have a __dict__, but copy.deepcopy() still works because it inspects the slot descriptors. However, if a slot contains a non-copyable object, you need to customize the copy behavior.

Deep Copy and Immutable Objects

Immutable objects like tuples and strings are not copied; they are returned as-is because there is no need to copy them. This is an optimization that copy.deepcopy() performs. It is safe because immutable objects cannot be modified.

When to Use Deep Copy vs Alternatives

Deep copy is not always the best tool. If you need to serialize an object to JSON or another format, you might use json.dumps() and json.loads() to create a deep copy, but this only works for JSON-serializable data and loses custom class types. For more complex objects, you might use pickle for serialization, but that has security and performance implications.

A manual copy constructor can be more efficient if you only need to copy a few fields. For example, if you have a class with a list and a string, you can write a method that creates a new instance and copies the list with list.copy(). This gives you fine-grained control and avoids the overhead of recursive traversal.

Use copy.deepcopy() when you need a general-purpose, reliable way to duplicate an arbitrary object graph without writing custom copy logic for every class. It is the standard tool for this job and is well-tested across many Python versions. Just be aware of its cost and its limitations with non-copyable resources.

Handling Deep Copy in Custom Data Structures

When you build your own container classes, you may want to control how deep copy behaves. For example, a class that wraps a list and maintains an index cache should copy the list but rebuild the cache. Implementing __deepcopy__ lets you do this cleanly.

import copy class IndexedList: def __init__(self, items): self.items = list(items) self.index = {item: i for i, item in enumerate(self.items)} def __deepcopy__(self, memo): new_list = IndexedList.__new__(IndexedList) new_list.items = copy.deepcopy(self.items, memo) new_list.index = {item: i for i, item in enumerate(new_list.items)} return new_list

This ensures that the copied object has a consistent index that matches the copied items. If you relied on the default deep copy, the index dictionary would be copied as-is, which might still point to the original items if they are not deep-copied correctly. Customizing __deepcopy__ gives you full control over the consistency of your data structure.

Another approach is to use the __reduce__ method, which is used by pickle and also by copy.deepcopy() when __deepcopy__ is not defined. Implementing __reduce__ can be more complex, but it allows you to control both pickling and copying with one method. In practice, __deepcopy__ is simpler for copy-specific logic.

Deep copy is a powerful feature, but it should be used deliberately. Understand what it does, when it is necessary, and how to customize it for your own classes. That knowledge prevents subtle bugs caused by unintended shared references and helps you write code that behaves predictably when objects are duplicated.

python deep copy: Practical Usage and Code Examples | RYUSLOG DEV