Python Shallow Copy vs Deep Copy: Understanding Object References with Practical Examples
Have you ever copied a Python dictionary, modified a nested list inside the copied object, and noticed that the original data also changed?
This is one of the common problems developers encounter when working with mutable objects in Python.
The reason behind this behavior is the difference between Shallow Copy and Deep Copy.
Python provides two mechanisms for copying objects through its built-in copy module. Although both create new objects, they handle nested objects differently.
Understanding this difference is important when building REST APIs, Django applications, FastAPI services, data processing pipelines, and large-scale backend systems.
In this article, we'll explore how shallow copy and deep copy work internally, how Python handles object references, and when to use each approach in real-world applications.
1. Understanding Object References in Python
Before discussing copying mechanisms, let's understand how Python manages objects.
When you assign one variable to another, Python generally does not create a new object. Both variables reference the same object.
Consider this example:
employee = {
"name": "Rahul",
"department": "Development"
}
employee_copy = employee
employee_copy["name"] = "Ashok"
print(employee["name"])
Output:
Ashok
Why did the original employee dictionary change?
Because both variables point to the same dictionary object.
You can visualize this relationship:
employee ───────┐
├────> Dictionary Object
employee_copy ──┘
This is called reference assignment.
When working with nested mutable objects, this behavior can introduce unexpected changes if developers do not understand how references are shared.
That's where shallow copy and deep copy become useful.
2. What Is Shallow Copy in Python?
A shallow copy creates a new outer object but does not recursively copy the objects contained inside it.
In other words, the outer container is independent, but nested objects are generally shared.
Python provides the copy.copy() function to perform shallow copying.
Let's understand this with an example.
import copy
employee = {
"name": "Rahul",
"skills": ["Python", "Django", "SQL"]
}
employee_copy = copy.copy(employee)
employee_copy["name"] = "Ashok"
employee_copy["skills"].append("FastAPI")
print("Original:", employee)
print("Copied:", employee_copy)
Output:
Original: {
'name': 'Rahul',
'skills': ['Python', 'Django', 'SQL', 'FastAPI']
}
Copied: {
'name': 'Ashok',
'skills': ['Python', 'Django', 'SQL', 'FastAPI']
}
Notice something interesting.
Changing the name value does not affect the original dictionary.
However, adding FastAPI to the nested skills list modifies both dictionaries.
Why?
Because the outer dictionaries are different objects, but the nested skills list is shared.
How Does Shallow Copy Work Internally?
The memory relationship looks like this:
Original Dictionary Shallow Copy
| |
v v
Outer Object Outer Object
| |
+----------+------------+
|
v
Skills List
Both dictionaries reference the same nested list.
This is why shallow copying is generally faster than deep copying.
Python only needs to create a new outer container and retain references to the existing elements.
Other Ways to Create Shallow Copies
Python provides several convenient ways to create shallow copies of built-in collections.
For lists:
numbers = [10, 20, 30]
new_numbers = numbers.copy()
You can also use slicing:
new_numbers = numbers[:]
For dictionaries:
employee = {"name": "Rahul", "age": 25}
employee_copy = employee.copy()
These approaches create shallow copies, not recursively independent nested structures.
3. What Is Deep Copy in Python?
A deep copy creates a new object and recursively copies its nested objects wherever supported.
Unlike shallow copying, deep copying attempts to create an independent copy of the complete object graph.
Python provides copy.deepcopy() for this purpose.
Let's modify our previous example.
import copy
employee = {
"name": "Rahul",
"skills": ["Python", "Django", "SQL"]
}
employee_copy = copy.deepcopy(employee)
employee_copy["name"] = "Ashok"
employee_copy["skills"].append("FastAPI")
print("Original:", employee)
print("Copied:", employee_copy)
Output:
Original: {
'name': 'Rahul',
'skills': ['Python', 'Django', 'SQL']
}
Copied: {
'name': 'Ashok',
'skills': ['Python', 'Django', 'SQL', 'FastAPI']
}
This time, the original dictionary remains unchanged.
The reason is that the nested skills list was also copied.
How Does Deep Copy Work Internally?
Deep copying involves more operations than shallow copying.
Python follows a recursive process to construct an independent object graph.
Conceptually:
Original Dictionary
|
v
Copy Outer Object
|
v
Examine Nested Objects
|
v
Copy Nested Objects
|
v
Reconstruct References
|
v
Return Deep Copy
Python also uses a memoization dictionary to remember objects that have already been copied.
This prevents repeatedly copying the same object and helps handle circular references.
4. Understanding Circular References in Deep Copy
Circular references occur when an object directly or indirectly references itself.
Consider this example:
import copy
data = []
data.append(data)
data_copy = copy.deepcopy(data)
print(data_copy is data_copy[0])
Output:
True
Why does this work?
Because Python's deep-copy mechanism uses memoization to track objects during the copying process.
Without this mechanism, recursive object relationships could result in infinite recursion.
Memoization also helps preserve shared-reference relationships inside the copied object graph.
This is an important implementation detail when working with complex Python objects.
5. Object Identity vs Object Equality
Another concept worth understanding is the difference between object identity and object equality.
Python provides two operators:
== → Checks whether two objects have equal values.
is → Checks whether two references point to the same object.
Let's see how this works.
import copy
original = [[10, 20], [30, 40]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
print(original == shallow)
print(original is shallow)
print(original == deep)
print(original is deep)
Output:
True
False
True
False
Both copied objects contain equivalent values, but they are different outer objects.
Now let's check their nested objects.
print(original[0] is shallow[0])
print(original[0] is deep[0])
Output:
True
False
This demonstrates the fundamental difference between shallow copying and deep copying.
Understanding is and == is particularly useful when debugging shared mutable state.
6. Performance: When Does Deep Copy Become Expensive?
One important consideration in production applications is performance.
Shallow copying generally requires less processing because it creates a new outer object without recursively duplicating nested objects.
Deep copying can require considerably more CPU time and memory, especially when working with large nested structures.
Imagine a backend application processing thousands of customer records.
Each customer object contains profile information, preferences, permissions, and transaction details.
If the application unnecessarily deep-copies every customer record, it may introduce additional memory allocations and processing overhead.
Let's measure the execution time.
import copy
import time
data = {
"users": [
{
"id": i,
"profile": {
"name": f"User{i}",
"skills": ["Python", "SQL", "Django"]
}
}
for i in range(10000)
]
}
start = time.perf_counter()
shallow = copy.copy(data)
shallow_time = time.perf_counter() - start
start = time.perf_counter()
deep = copy.deepcopy(data)
deep_time = time.perf_counter() - start
print("Shallow Copy:", shallow_time)
print("Deep Copy:", deep_time)
The actual execution time depends on hardware, Python version, and object structure.
For this example, deep copying performs significantly more work because it traverses the nested data structure.
How Can Developers Optimize Copying Performance?
A practical approach is to avoid copying objects unnecessarily.
If only one nested dictionary needs modification, consider copying that specific dictionary instead of the complete object graph.
Immutable data structures can also reduce accidental modifications and unnecessary copying.
For performance investigations, Python developers can use:
→ timeit for benchmarking individual operations.
→ cProfile for application-level performance analysis.
→ tracemalloc for tracking memory allocations.
→ memory_profiler for investigating memory consumption.
The important point is to measure actual application behavior rather than assume that every deep copy is expensive or every shallow copy is sufficient.
7. Real-World Use Cases in Python Backend Development
Let's move beyond isolated examples and understand where these concepts are useful in real projects.
Use Case 1: Django and Flask Configuration Management
Backend applications frequently maintain configuration dictionaries containing database settings, logging options, and application-level preferences.
Consider a default configuration:
import copy
DEFAULT_CONFIG = {
"database": {
"host": "localhost",
"port": 5432
},
"logging": {
"level": "INFO"
}
}
def get_config():
return copy.deepcopy(DEFAULT_CONFIG)
config = get_config()
config["database"]["host"] = "production-db"
Deep copying ensures that changes to an individual configuration instance do not unintentionally modify the shared default configuration.
However, avoid deep-copying live database connections, open files, and other external resources.
Use Case 2: REST API Request Processing
Consider an e-commerce application receiving customer order information through a REST API.
The request contains customer details, product information, and delivery settings.
Sometimes the application needs to transform this information without modifying the original payload.
import copy
order = {
"order_id": 101,
"customer": {
"name": "Rahul"
},
"items": [
{
"product": "Laptop",
"quantity": 1
}
]
}
processed_order = copy.deepcopy(order)
processed_order["customer"]["name"] = "Updated Customer"
The processed order can now be modified independently.
However, if the original payload is never modified, creating an additional copy may not provide any real benefit.
Use Case 3: Data Processing and Analytics
Python developers frequently work with NumPy arrays, Pandas DataFrames, and nested metadata structures.
Copying behavior can differ between these libraries and Python's standard copy module.
For example, Pandas provides:
df_copy = df.copy(deep=True)
This method follows Pandas-specific copying semantics. It should not be assumed to provide exactly the same behavior as copy.deepcopy() for every object stored inside a DataFrame.
When working with large datasets, understanding library-specific memory management is essential.
Use Case 4: Multi-Threaded Applications
Shared mutable objects can introduce concurrency problems.
Consider a worker thread that needs an independent copy of application settings.
import copy
import threading
shared_data = {
"settings": {
"status": "active"
}
}
def process_data():
local_data = copy.deepcopy(shared_data)
local_data["settings"]["status"] = "processed"
thread = threading.Thread(target=process_data)
thread.start()
thread.join()
The worker modifies its independent copy instead of changing the original nested dictionary.
One important consideration: deep copying does not automatically make concurrent access thread-safe. Synchronization may still be necessary if another thread modifies the original object during copying.
8. Common Mistakes Python Developers Should Avoid
Understanding copying behavior is important, but knowing what not to do is equally valuable.
Mistake 1: Assuming assignment creates a new object
a = [1, 2, 3]
b = a
b.append(4)
print(a)
Output:
[1, 2, 3, 4]
Both variables reference the same list.
Mistake 2: Using shallow copy for nested mutable objects
import copy
original = {
"profile": {
"skills": ["Python"]
}
}
new_data = copy.copy(original)
new_data["profile"]["skills"].append("Django")
The original nested list also changes.
Mistake 3: Deep-copying everything
Deep copying creates additional objects and may increase memory consumption and execution time.
Use it only when independent nested objects are actually required.
Mistake 4: Ignoring custom object behavior
Custom Python classes can define their own copying logic through:
__copy__() → Controls shallow-copy behavior.
__deepcopy__(memo) → Controls deep-copy behavior.
This is useful when objects contain resources or attributes that require specialized handling.
9. Best Practices for Enterprise Python Applications
In enterprise applications, copying decisions should be based on object ownership, mutation requirements, and performance expectations.
Prefer immutable application models when appropriate because they reduce accidental state changes.
Use explicit copying strategies so developers can understand whether nested objects are shared or independently copied.
Avoid copying database connections, sockets, file handles, and thread locks without understanding their specific behavior.
For large nested structures, consider copying only the portions that need modification.
Also, write unit tests that verify whether modifying copied nested objects affects the original data.
For custom classes, implement copying methods carefully and preserve object relationships, including circular references where applicable.
These practices help prevent difficult-to-debug issues in production systems.
10. Frequently Asked Python Interview Questions
Q1. What is the difference between shallow copy and deep copy?
Shallow copy creates a new outer object but retains references to nested objects. Deep copy recursively copies nested objects wherever supported.
Q2. Which module provides shallow and deep copying?
Python provides the built-in copy module.
Q3. Does shallow copy duplicate nested dictionaries?
No. Nested dictionaries are generally shared references.
Q4. Why does deep copy use memoization?
Memoization prevents repeated copying, handles circular references, and preserves shared-reference relationships.
Q5. Is deep copy always slower than shallow copy?
For typical nested mutable structures, deep copying requires more processing. Actual performance depends on the object types and copying implementations.
Q6. Can deep copy handle circular references?
Yes. Python uses a memo dictionary to manage circular references.
Q7. Can developers customize copying behavior?
Yes. Custom classes can implement __copy__() and __deepcopy__(memo).
Conclusion
Shallow copy and deep copy are fundamental concepts in Python object management.
Shallow copying creates a new outer object while retaining references to nested objects. Deep copying recursively duplicates nested objects wherever supported, providing greater independence at the cost of additional processing and memory.
As a developer, you should not automatically choose deep copy whenever you need a duplicate object.
Understand the structure of your data, identify mutable objects, evaluate performance requirements, and select the copying strategy that provides the required level of isolation.
If you are working with Python, Django, Flask, FastAPI, or data processing applications, mastering these concepts will help you write more predictable and maintainable code.
Keep experimenting with nested lists, dictionaries, and custom classes. Understanding how Python handles object references is one of the important steps toward becoming a stronger Python developer.
Take Your Python Full Stack Development Skills Further
Understanding Python object references, shallow copying, deep copying, and memory management is an important part of becoming a skilled Python developer.
However, professional Python development involves much more than Python fundamentals. Developers also need practical experience with Django, Flask, FastAPI, REST API development, database integration, JavaScript, React, and real-world application development.
If you are looking to build these skills, exploring a Python Full Stack Online course in Hyderabad can help you understand both backend and frontend development through structured learning and practical projects.
At Ashok IT, learners can explore Python Full Stack development concepts, work with practical programming examples, understand application architecture, and strengthen their technical knowledge.
Whether you are a student, fresher, or working professional, building hands-on experience with Python technologies can help you prepare for Python developer and Full Stack developer opportunities.
Top comments (0)