DEV Community

Cover image for Python Shallow Copy vs Deep Copy:
shalini
shalini

Posted on

Python Shallow Copy vs Deep Copy:

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"])
Enter fullscreen mode Exit fullscreen mode

Output:

Ashok
Enter fullscreen mode Exit fullscreen mode

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 ──┘
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

Original: {
    'name': 'Rahul',
    'skills': ['Python', 'Django', 'SQL', 'FastAPI']
}

Copied: {
    'name': 'Ashok',
    'skills': ['Python', 'Django', 'SQL', 'FastAPI']
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

You can also use slicing:

new_numbers = numbers[:]
Enter fullscreen mode Exit fullscreen mode

For dictionaries:

employee = {"name": "Rahul", "age": 25}

employee_copy = employee.copy()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

Original: {
    'name': 'Rahul',
    'skills': ['Python', 'Django', 'SQL']
}

Copied: {
    'name': 'Ashok',
    'skills': ['Python', 'Django', 'SQL', 'FastAPI']
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

Output:

True
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

True
False
True
False
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

Output:

True
False
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

[1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)