Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Python FAQ: Top Questions

35. What is the difference between shallow copy and deep copy in Python?

In Python, when you copy an object, it's crucial to understand whether you're performing a **shallow copy** or a **deep copy**. The difference lies in how nested objects (objects contained within the original object) are handled.

The `copy` module in Python provides functions for both types of copying: `copy.copy()` for shallow copy and `copy.deepcopy()` for deep copy.

1. Shallow Copy:

  • Mechanism: A shallow copy creates a new compound object (e.g., a new list, a new dictionary), but it does not create copies of the nested objects contained within the original. Instead, it populates the new compound object with *references* to the same nested objects that the original object contains.
  • Implication: If you modify a nested object in the shallow copy, the changes will also be reflected in the original object (and vice-versa) because both the original and the copy are pointing to the *same* nested object. Changes to the top-level container (e.g., adding/removing elements from the list) will not affect the other.
  • How to create:
    • `copy.copy(obj)`
    • For lists: `new_list = old_list[:]` or `list(old_list)`
    • For dictionaries: `new_dict = old_dict.copy()` or `dict(old_dict)`
    • For sets: `new_set = old_set.copy()` or `set(old_set)`
  • Analogy: Imagine a folder (outer object) containing documents. A shallow copy creates a new folder, but instead of making copies of the documents, it puts *shortcuts* to the original documents into the new folder. If you edit a document via a shortcut in the new folder, the original document changes because both shortcuts point to the same physical file.

2. Deep Copy:

  • Mechanism: A deep copy creates a new compound object and then recursively creates copies of all nested objects found in the original. It aims to make the copy entirely independent of the original.
  • Implication: If you modify a nested object in the deep copy, the changes will **not** be reflected in the original object because the deep copy has its own, separate copies of all nested objects.
  • How to create: `copy.deepcopy(obj)`
  • Considerations:
    • **Performance:** Deep copying can be significantly slower than shallow copying, especially for large and complex objects, as it needs to traverse the entire object graph.
    • **Recursion Limit:** For very deeply nested objects, it can hit Python's recursion limit.
    • **Reference Cycles:** `copy.deepcopy()` handles recursive objects (objects that refer to themselves or form cycles) to prevent infinite recursion during copying.
    • **Custom Objects:** For custom classes, you might need to implement `__copy__` and `__deepcopy__` methods if the default behavior is not sufficient or if the object has external resources that need special handling during copying.
  • Analogy: Using the folder analogy, a deep copy creates a new folder and then makes *full, independent copies* of all the documents inside the original folder, placing them into the new folder. Now, if you edit a document in the new folder, the original document remains untouched because they are separate files.

Summary Table:

Feature Shallow Copy (`copy.copy()`) Deep Copy (`copy.deepcopy()`)
Nested Objects References to original nested objects New copies of nested objects (recursive)
Independence Not fully independent (changes to nested objects affect both) Fully independent (changes to nested objects do not affect original)
Memory Usage Generally lower (reuses nested objects) Generally higher (creates new copies of everything)
Performance Faster Slower (due to recursion and object creation)
Handles Cycles No explicit handling (not an issue as it doesn't recurse) Yes, explicitly handles recursive objects to prevent infinite loops
Use Case When modifying top-level structure or when nested objects are immutable When you need a completely independent replica, including nested objects

Choosing between shallow and deep copy depends entirely on your requirements. If you only need to copy the top-level container and shared nested objects are acceptable (or nested objects are immutable), a shallow copy is sufficient and more efficient. If you need a completely independent copy of the entire object graph, a deep copy is necessary.


import copy

# --- Example 1: Shallow Copy ---
print("--- Shallow Copy Example ---")

original_list = [[1, 2, 3], [4, 5, 6]]
shallow_copied_list = copy.copy(original_list)

print(f"Original list:           {original_list}, id: {id(original_list)}")
print(f"Shallow copied list:     {shallow_copied_list}, id: {id(shallow_copied_list)}")
print(f"Are lists the same object? {original_list is shallow_copied_list}") # False (new top-level list)

# Check nested objects
print(f"Original nested list 0 id: {id(original_list[0])}")
print(f"Shallow copied nested list 0 id: {id(shallow_copied_list[0])}")
print(f"Are nested lists the same object? {original_list[0] is shallow_copied_list[0]}") # True (same nested object)

# Modify the shallow copy
shallow_copied_list.append([7, 8, 9]) # Add new element to top-level copy
shallow_copied_list[0][0] = 99        # Modify nested object (shared)

print(f"\nAfter modification:")
print(f"Original list:           {original_list}") # Nested list changed, top-level remains same
print(f"Shallow copied list:     {shallow_copied_list}") # Nested list changed, top-level changed

# Observe: 99 is in both original_list and shallow_copied_list's first nested list.
# [7, 8, 9] is only in shallow_copied_list.


# --- Example 2: Deep Copy ---
print("\n--- Deep Copy Example ---")

original_dict = {
    'name': 'Data',
    'values': {'a': 1, 'b': [2, 3]},
    'items': [10, 20]
}
deep_copied_dict = copy.deepcopy(original_dict)

print(f"Original dict:           {original_dict}, id: {id(original_dict)}")
print(f"Deep copied dict:        {deep_copied_dict}, id: {id(deep_copied_dict)}")
print(f"Are dicts the same object? {original_dict is deep_copied_dict}") # False (new top-level dict)

# Check nested objects
print(f"Original 'values' dict id: {id(original_dict['values'])}")
print(f"Deep copied 'values' dict id: {id(deep_copied_dict['values'])}")
print(f"Are nested 'values' dicts the same object? {original_dict['values'] is deep_copied_dict['values']}") # False (new nested object)

print(f"Original 'items' list id: {id(original_dict['items'])}")
print(f"Deep copied 'items' list id: {id(deep_copied_dict['items'])}")
print(f"Are nested 'items' lists the same object? {original_dict['items'] is deep_copied_dict['items']}") # False (new nested object)


# Modify the deep copy
deep_copied_dict['name'] = 'New Data'
deep_copied_dict['values']['a'] = 999
deep_copied_dict['values']['b'].append(4) # Modify deeply nested list
deep_copied_dict['items'].append(30)

print(f"\nAfter modification:")
print(f"Original dict:           {original_dict}") # No changes to original
print(f"Deep copied dict:        {deep_copied_dict}") # All changes are isolated


# --- Example 3: Copying Custom Objects with Nested Objects ---
print("\n--- Custom Objects with Nested Objects ---")

class Wallet:
    def __init__(self, currency, balance):
        self.currency = currency
        self.balance = balance

class Person:
    def __init__(self, name, wallet):
        self.name = name
        self.wallet = wallet

    def __repr__(self):
        return f"Person(name='{self.name}', wallet={self.wallet.currency} {self.wallet.balance})"

# Create original objects
wallet1 = Wallet("USD", 100)
person1 = Person("Alice", wallet1)

print(f"Original person: {person1}")
print(f"Original wallet ID: {id(person1.wallet)}")

# Shallow copy
person_shallow_copy = copy.copy(person1)
print(f"Shallow copy of person: {person_shallow_copy}")
print(f"Shallow copied wallet ID: {id(person_shallow_copy.wallet)}")
print(f"Wallet is same object in shallow copy? {person1.wallet is person_shallow_copy.wallet}") # True

person_shallow_copy.name = "Bob"
person_shallow_copy.wallet.balance += 50 # Modifies the SHARED wallet

print(f"\nAfter shallow copy modification:")
print(f"Original person: {person1}")
print(f"Shallow copy of person: {person_shallow_copy}")
# Notice Alice's balance also changed!

# Deep copy
wallet2 = Wallet("EUR", 200)
person2 = Person("Charlie", wallet2)

person_deep_copy = copy.deepcopy(person2)
print(f"\nOriginal person: {person2}")
print(f"Original wallet ID: {id(person2.wallet)}")
print(f"Deep copy of person: {person_deep_copy}")
print(f"Deep copied wallet ID: {id(person_deep_copy.wallet)}")
print(f"Wallet is same object in deep copy? {person2.wallet is person_deep_copy.wallet}") # False

person_deep_copy.name = "David"
person_deep_copy.wallet.balance += 100 # Modifies the independent wallet

print(f"\nAfter deep copy modification:")
print(f"Original person: {person2}")
print(f"Deep copy of person: {person_deep_copy}")
# Notice Charlie's balance is unchanged!
        

Explanation of the Example Code:

  • **Shallow Copy Example:**
    • `original_list` contains two nested lists.
    • `shallow_copied_list = copy.copy(original_list)` creates a new top-level list.
    • `original_list is shallow_copied_list` is `False`, confirming they are different objects.
    • However, `original_list[0] is shallow_copied_list[0]` is `True`, confirming that the *nested lists* are the same objects. They are shared.
    • When `shallow_copied_list[0][0] = 99` is executed, this change affects the nested list that is shared. Therefore, `original_list[0][0]` also becomes `99`.
    • Adding `[7, 8, 9]` to `shallow_copied_list` only affects the top-level copied list, not the original, as the top-level containers are distinct.
  • **Deep Copy Example:**
    • `original_dict` has nested dictionaries and lists.
    • `deep_copied_dict = copy.deepcopy(original_dict)` creates a new top-level dictionary and recursively creates new copies of all nested dictionaries and lists.
    • `original_dict is deep_copied_dict` is `False`.
    • Crucially, `original_dict['values'] is deep_copied_dict['values']` and `original_dict['items'] is deep_copied_dict['items']` are both `False`. This confirms that the nested objects are distinct copies.
    • Modifications to `deep_copied_dict` (including its nested elements) have no effect on `original_dict`, demonstrating complete independence.
  • **Custom Objects with Nested Objects:**
    • The `Person` class has a `Wallet` object as an attribute, representing a nested object.
    • **Shallow Copy:** When `person_shallow_copy = copy.copy(person1)` is performed, `person_shallow_copy` is a new `Person` object, but its `wallet` attribute *still points to the original `wallet1` object*. So, `person1.wallet is person_shallow_copy.wallet` is `True`. Any change to `person_shallow_copy.wallet.balance` directly affects `person1.wallet.balance`.
    • **Deep Copy:** When `person_deep_copy = copy.deepcopy(person2)` is performed, `person_deep_copy` is a new `Person` object, AND its `wallet` attribute points to a *brand new `Wallet` object* that is a copy of `wallet2`. So, `person2.wallet is person_deep_copy.wallet` is `False`. Changes to `person_deep_copy.wallet.balance` are isolated and do not affect `person2.wallet.balance`.

These examples clearly illustrate the fundamental difference between shallow and deep copies, emphasizing that deep copies are necessary when complete independence of an object and all its nested mutable components is required.