How to deep copy in Python
Learn how to deep copy in Python. Explore different methods, tips, real-world applications, and how to debug common errors.

To duplicate objects in Python, you can use a deep copy. This process creates a new, independent object with its own copies of all nested objects to prevent unintended side effects.
In this article, you'll explore techniques to perform a deep copy with the copy module. You will also discover practical tips, real-world applications, and advice to debug common copy-related issues.
Using copy.deepcopy() for basic deep copying
import copy
original = [1, [2, 3], {'a': 4}]
deep_copied = copy.deepcopy(original)
original[1][0] = 'changed'
print(f"Original: {original}")
print(f"Deep copy: {deep_copied}")--OUTPUT--Original: [1, ['changed', 3], {'a': 4}]
Deep copy: [1, [2, 3], {'a': 4}]
The code uses copy.deepcopy() to create a fully independent duplicate of the original list, which contains nested objects. The function recursively copies every element, ensuring no link remains to the source object.
The output confirms why this is useful:
- Modifying the nested list inside
originalhas no impact ondeep_copied. - This isolation prevents unintended side effects, a common issue when you're only creating shallow copies of complex data structures.
Alternative deep copying methods
Although copy.deepcopy() is the standard solution, you can also create deep copies through serialization with pickle and json or by writing the logic yourself.
Using pickle for deep copying
import pickle
original = [1, [2, 3], {'a': 4}]
deep_copied = pickle.loads(pickle.dumps(original))
original[1][0] = 'changed'
print(f"Original: {original}")
print(f"Deep copy: {deep_copied}")--OUTPUT--Original: [1, ['changed', 3], {'a': 4}]
Deep copy: [1, [2, 3], {'a': 4}]
The pickle module offers another way to create a deep copy through a process called serialization. First, pickle.dumps() converts the object into a byte stream. Then, pickle.loads() reconstructs that stream into a brand new object in memory.
- This serialize-deserialize cycle effectively creates a full duplicate. The resulting object is completely independent, so modifications to the original won't affect the copy, just like with
copy.deepcopy().
Using json for deep copying
import json
original = [1, [2, 3], {'a': 4}]
deep_copied = json.loads(json.dumps(original))
original[1][0] = 'changed'
print(f"Original: {original}")
print(f"Deep copy: {deep_copied}")--OUTPUT--Original: [1, ['changed', 3], {'a': 4}]
Deep copy: [1, [2, 3], {'a': 4}]
Similar to pickle, the json module can create a deep copy through serialization. The json.dumps() function first converts the object into a JSON string. Then, json.loads() parses that string to build a completely new object in memory.
- This approach is straightforward but has a key limitation. It only works with data types that JSON natively supports, like lists, dictionaries, strings, and numbers. It can't handle more complex Python objects, such as custom classes.
Manual deep copying for simple structures
original = [1, [2, 3], {'a': 4}]
deep_copied = [
original[0],
original[1][:], # List slicing creates a copy
{k: v for k, v in original[2].items()}
]
original[1][0] = 'changed'
print(f"Original: {original}")
print(f"Deep copy: {deep_copied}")--OUTPUT--Original: [1, ['changed', 3], {'a': 4}]
Deep copy: [1, [2, 3], {'a': 4}]
If you know an object’s exact structure, you can build a deep copy manually. This approach gives you precise control but is best for simple data structures where you can anticipate the nesting level.
- List slicing with
[:]creates a shallow copy of the nested list. - A dictionary comprehension,
{k: v for k, v in original[2].items()}, builds an entirely new dictionary.
Because the nested objects here only contain immutable types, this method works. However, it quickly becomes cumbersome and error-prone with more complex objects—making copy.deepcopy() a safer bet in most cases.
Advanced deep copying techniques
When standard methods fall short, you can gain more control by customizing the deep copy process for your objects or optimizing it for large datasets.
Customizing deep copying with __deepcopy__
import copy
class CustomObject:
def __init__(self, value):
self.value = value
def __deepcopy__(self, memo):
print("Custom deepcopy called")
return CustomObject(copy.deepcopy(self.value, memo))
obj = CustomObject([1, 2, 3])
obj_copy = copy.deepcopy(obj)
print(f"Original: {obj.value}")
print(f"Deep copy: {obj_copy.value}")--OUTPUT--Custom deepcopy called
Original: [1, 2, 3]
Deep copy: [1, 2, 3]
You can customize how copy.deepcopy() behaves for your own classes by implementing the __deepcopy__ special method. When copy.deepcopy() is called on an instance of CustomObject, it doesn't use its default logic. Instead, it invokes your custom method, giving you full control over the duplication process.
- The
__deepcopy__method receives amemodictionary, which tracks already copied objects to prevent infinite loops from circular references. - Inside, you define exactly how to create the new object. Here, it constructs a new
CustomObject, passing a deep copy of the original object'svalue.
Deep copying with context managers
import copy
import contextlib
@contextlib.contextmanager
def deepcopy_context():
original_deepcopy = copy.deepcopy
try:
def custom_deepcopy(x, memo=None):
print("Using custom deepcopy")
return original_deepcopy(x, memo)
copy.deepcopy = custom_deepcopy
yield
finally:
copy.deepcopy = original_deepcopy
with deepcopy_context():
obj_copy = copy.deepcopy([1, 2, 3])--OUTPUT--Using custom deepcopy
A context manager, created with the @contextlib.contextmanager decorator, lets you temporarily change copy.deepcopy()'s behavior. It's useful for adding logging or other custom logic within a specific block of code without permanently altering the function.
- When you enter the
withblock, the context manager replaces the standardcopy.deepcopy()with a custom version. - Inside the block, any call to
copy.deepcopy()executes your custom logic. Once the block finishes, thefinallyclause automatically restores the original function.
Optimizing deep copying for large data structures
import copy
import time
large_list = list(range(10000))
shared_item = [0] * 1000
# Without memoization
start = time.time()
copy.deepcopy([large_list, large_list])
print(f"Without memoization: {time.time() - start:.4f} seconds")
# With memoization (built into deepcopy)
start = time.time()
memo = {}
copy.deepcopy([large_list, large_list], memo)
print(f"With memoization: {time.time() - start:.4f} seconds")--OUTPUT--Without memoization: 0.0121 seconds
With memoization: 0.0062 seconds
When copying large data structures, copy.deepcopy() uses an internal optimization called memoization to avoid redundant work. It keeps a record of objects it has already duplicated during a single copy operation.
- If the function encounters the same object more than once—like the
large_listthat appears twice in the example—it won't copy it again. Instead, it reuses the reference to the copy it already created. This simple caching makes the process significantly faster, especially with objects that are referenced multiple times.
Move faster with Replit
Replit is an AI-powered development platform that transforms natural language into working applications. Describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.
For the deep copying techniques we've explored, Replit Agent can turn them into production tools:
- Build a session history manager for a web app that saves snapshots of user state, allowing for reliable undo and redo functionality.
- Create a sandbox environment for testing API configurations, where each test run gets a deep copy of the default settings to modify without side effects.
- Deploy a multi-step data transformation utility that duplicates complex objects at each stage, ensuring data integrity throughout the pipeline.
Describe your app idea, and Replit Agent writes the code, tests it, and fixes issues automatically. It’s a faster way to move from concept to a deployed application.
Common errors and challenges
Deep copying is powerful, but you can run into a few common pitfalls if you're not careful.
It's a simple mistake, but forgetting to add import copy at the top of your file is a frequent source of errors. If you call copy.deepcopy() without the import, Python will raise a NameError because it doesn't recognize the copy module. The fix is straightforward—just make sure the import statement is included before you use the function.
Circular references occur when an object contains a reference to itself, creating a loop that can cause an infinite recursion error during a copy. Imagine an object a that references object b, and b in turn references a back—a naive copy process would get stuck copying them forever. Fortunately, copy.deepcopy() is designed to handle this automatically.
- It uses a
memodictionary to keep track of every object it has already copied during the operation. - If it encounters an object a second time, it doesn't try to copy it again but instead uses the reference to the already created copy.
- This prevents infinite loops and ensures the copy process completes successfully, even with complex, self-referencing structures.
While using json.dumps() and json.loads() is a clever trick for deep copying, it's important to remember its limits. This method only works for JSON-serializable data types, like strings, numbers, lists, and dictionaries. If you try to copy an object containing unsupported types, such as a custom class instance, the process will fail because the json module doesn't know how to convert it into a string. For anything beyond basic data structures, sticking with copy.deepcopy() is the safer and more reliable option.
Forgetting to import the copy module
It's a simple but common mistake. Before you can use functions from the copy module, you must import it. If you call deepcopy() without the import, Python raises a NameError because it doesn't recognize the function. The following code demonstrates this error.
original = [1, [2, 3], {'a': 4}]
deep_copied = deepcopy(original) # NameError: name 'deepcopy' is not defined
original[1][0] = 'changed'
print(f"Original: {original}")
print(f"Deep copy: {deep_copied}")
This code fails with a NameError because deepcopy() is called directly. Python doesn't know this function exists until you explicitly import its module. The following example shows the simple fix.
import copy
original = [1, [2, 3], {'a': 4}]
deep_copied = copy.deepcopy(original)
original[1][0] = 'changed'
print(f"Original: {original}")
print(f"Deep copy: {deep_copied}")
By adding import copy at the top, you make the module's functions available to your script. The corrected code then calls the function using its full name, copy.deepcopy(), which tells Python exactly where to find it. This resolves the NameError by providing the necessary context. This kind of error is common when you move code between files or forget to include the required imports for a standard library module.
Handling circular references in deep copying
A circular reference—where an object refers back to itself—can easily trap a copy function in an infinite loop. Fortunately, copy.deepcopy() is built to handle this scenario gracefully. The code below creates this exact situation to show how it's managed.
import copy
# Create a circular reference
a = [1, 2, 3]
a.append(a) # a contains a reference to itself
# Make multiple deep copies without realizing there's a circular reference
copies = [copy.deepcopy(a) for _ in range(10)]
print(f"Created {len(copies)} copies")
The list a includes a reference to itself, creating a circular dependency. The code then attempts to duplicate this complex object multiple times in a loop. The next example shows how copy.deepcopy() handles this situation internally.
import copy
# Create a circular reference
a = [1, 2, 3]
a.append(a) # a contains a reference to itself
# Use a shared memo dictionary for efficient copying
memo = {}
copies = [copy.deepcopy(a, memo) for _ in range(10)]
print(f"Created {len(copies)} copies efficiently")
While copy.deepcopy() automatically handles circular references, you can make it more efficient when copying the same object multiple times. By passing your own memo dictionary to the function, you share the cache of copied objects across different calls.
- The first time
copy.deepcopy()runs, it duplicates the object and saves it inmemo. - Subsequent calls find the object in
memoand reuse the copy, avoiding redundant work.
Understanding the limitations of json for deep copying
The json serialization trick for deep copying is convenient, but it's not a universal solution. It only supports standard data types like lists and dictionaries. When you try to copy a custom Python object, the process fails. The code below shows this limitation in action.
import json
class User:
def __init__(self, name, age):
self.name = name
self.age = age
user = User("Alice", 30)
# Try to deep copy using json
user_copy = json.loads(json.dumps(user)) # TypeError: Object of type User is not JSON serializable
The json.dumps() function raises a TypeError because it cannot convert the custom User object into a string. The module only understands standard data types. The following code demonstrates how to resolve this limitation.
import copy
class User:
def __init__(self, name, age):
self.name = name
self.age = age
user = User("Alice", 30)
# Use copy.deepcopy for objects
user_copy = copy.deepcopy(user)
print(f"Original name: {user.name}, Copy name: {user_copy.name}")
The fix is to use copy.deepcopy(), which is designed to handle any Python object, including custom classes like User. Unlike the json method, it doesn't rely on serialization to a string format and can duplicate complex object structures directly. You should watch for this issue whenever your data contains more than just basic types like lists, dictionaries, or numbers. For custom objects, copy.deepcopy() is the reliable choice.
Real-world applications
Beyond troubleshooting errors, copy.deepcopy() is essential for building robust features like managing application settings and tracking game states.
Using deepcopy() for application settings management
When managing application settings, deepcopy() allows you to give each user a personal copy of the defaults, ensuring their changes won't affect the original configuration.
import copy
default_settings = {
"theme": "dark",
"notifications": {"email": True, "sms": False},
"languages": ["en", "es"]
}
user_settings = copy.deepcopy(default_settings)
user_settings["theme"] = "light"
user_settings["notifications"]["sms"] = True
print(f"User settings: {user_settings}")
print(f"Default settings (unchanged): {default_settings}")
This code shows how copy.deepcopy() creates a completely independent duplicate of a nested dictionary. The user_settings object is an entirely new entity, so any changes you make to it won't alter the original default_settings.
- The function recursively copies all elements, including the nested
notificationsdictionary. - When you modify
user_settings["notifications"]["sms"], you're only changing the copy. - This leaves the original
default_settingsuntouched, making it a safe template for creating new configurations.
Using deepcopy() in game state management
In game development, deepcopy() is essential for creating checkpoints because it saves a complete, independent snapshot of the game state at a specific moment.
import copy
game_state = {
"player": {"health": 100, "inventory": ["sword", "potion"]},
"enemies": [{"type": "goblin", "health": 30}, {"type": "troll", "health": 100}],
"level": 3
}
# Save game checkpoint before boss fight
checkpoint = copy.deepcopy(game_state)
# Player loses health and uses potion in battle
game_state["player"]["health"] -= 40
game_state["player"]["inventory"].remove("potion")
print(f"Current state: {game_state}")
print(f"Checkpoint state: {checkpoint}")
This code uses copy.deepcopy() to save a complete snapshot of the game's current state before a major event. The game_state dictionary contains nested data, like the player's inventory list. After the checkpoint is created, the live game_state is modified to reflect battle events.
- Because the copy is deep, changes to the player's health and inventory don't affect the
checkpoint. - This ensures the saved state is a true, isolated record, allowing you to reliably restore the game to that exact moment if the player fails.
Get started with Replit
Turn your knowledge into a real tool with Replit Agent. Describe what you want, like “a settings editor that deep copies a default config” or “a game state manager with a checkpoint system,” and watch it get built.
Replit Agent writes the code, tests for errors, and deploys your application automatically. It handles the heavy lifting so you can focus on your ideas. Start building with Replit.
Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.
Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.


.png)
.png)