Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Python FAQ: Top Questions

12. Explain the difference between shallow copy and deep copy.

When working with mutable objects (like lists or dictionaries) in Python, simply assigning one variable to another creates a reference, not a copy. Both variables will point to the *same object* in memory. To create an independent copy, you need to perform either a shallow copy or a deep copy. The choice between them depends on whether your object contains nested mutable structures and how you want changes to those structures to be reflected.

1. Shallow Copy (`copy.copy()`):

  • Concept: A shallow copy creates a new compound object, but it does *not* create copies of nested objects (like lists within lists, or dictionaries within dictionaries). Instead, it inserts *references* to the nested objects found in the original into the new copy.
  • Behavior:
    • If the original object contains only immutable items (numbers, strings, tuples), a shallow copy behaves like a deep copy because immutable items cannot be changed.
    • If the original object contains **mutable nested objects**, changes made to these nested objects through either the original or the shallow copy will be visible in *both* the original and the shallow copy, because they both refer to the same nested object in memory.
    • Changes to the *top-level* elements of the shallow copy (e.g., assigning a new integer to an index in a list) will *not* affect the original, as the shallow copy is a distinct object at the top level.
  • Methods for Shallow Copy:
    • Using the `copy` module: `copy.copy(obj)`
    • Slicing for lists: `new_list = old_list[:]` or `new_list = list(old_list)`
    • Using the `dict()` constructor for dictionaries: `new_dict = dict(old_dict)`

2. Deep Copy (`copy.deepcopy()`):

  • Concept: A deep copy creates a completely independent copy of the original object and **recursively creates copies of all nested objects** within it. It ensures that changes to the deep copy (including changes to its nested objects) do not affect the original object, and vice-versa.
  • Behavior:
    • The deep copy is a truly independent duplicate.
    • If the original object contains mutable nested objects, changes made to these nested objects in the deep copy will **not** affect the original object, and vice versa. They are entirely separate entities.
    • This is achieved by traversing the original object and creating new copies of everything it contains, down to the deepest level.
  • Method for Deep Copy:
    • Using the `copy` module: `copy.deepcopy(obj)`
  • Considerations:
    • Deep copying can be computationally more expensive and memory-intensive than shallow copying, especially for very large or deeply nested objects.
    • It can also encounter issues with circular references (where objects directly or indirectly refer to themselves), though `copy.deepcopy()` handles these by default to prevent infinite recursion.

In essence, a shallow copy copies the object itself and its immediate references, while a deep copy copies the object and all objects it references, recursively.


import copy

# --- Original Object (with nested mutable list) ---
original_list = [1, 2, [3, 4], 5]
print(f"Original List: {original_list}")
print(f"ID of original_list: {id(original_list)}")
print(f"ID of nested list in original: {id(original_list[2])}")

# --- 1. Reference Assignment (NOT a copy) ---
print("\n--- 1. Reference Assignment ---")
ref_list = original_list
print(f"Ref List (points to original): {ref_list}")
print(f"ID of ref_list: {id(ref_list)}") # Same ID as original_list

# Modify ref_list's nested element
ref_list[2].append(99) # Changes the nested list which is shared
print(f"Original List after ref_list modification: {original_list}")
print(f"Ref List after ref_list modification: {ref_list}")
# Both show [1, 2, [3, 4, 99], 5] - they are the same object


# --- 2. Shallow Copy ---
print("\n--- 2. Shallow Copy (using copy.copy()) ---")
shallow_copy_list = copy.copy(original_list)
print(f"Shallow Copy List: {shallow_copy_list}")
print(f"ID of shallow_copy_list: {id(shallow_copy_list)}") # Different ID (new top-level object)
print(f"ID of nested list in shallow copy: {id(shallow_copy_list[2])}") # Same ID as original's nested list

# Modify shallow_copy_list's top-level element
shallow_copy_list[0] = 100
print(f"Original List after shallow_copy_list top-level change: {original_list}")
print(f"Shallow Copy List after shallow_copy_list top-level change: {shallow_copy_list}")
# Original: [1, 2, [3, 4, 99], 5]
# Shallow:  [100, 2, [3, 4, 99], 5]
# Top level changed independently.

# Modify shallow_copy_list's nested mutable element
# This affects both original and shallow_copy because the nested list is shared!
shallow_copy_list[2].append(88)
print(f"Original List after shallow_copy_list nested change: {original_list}")
print(f"Shallow Copy List after shallow_copy_list nested change: {shallow_copy_list}")
# Original: [1, 2, [3, 4, 99, 88], 5]
# Shallow:  [100, 2, [3, 4, 99, 88], 5]
# Nested list changed in BOTH.


# --- 3. Deep Copy ---
print("\n--- 3. Deep Copy (using copy.deepcopy()) ---")
# Re-initialize original for clear demonstration
original_list_for_deep = [1, 2, [6, 7], 8]
print(f"Original List for Deep Copy: {original_list_for_deep}")
print(f"ID of original_list_for_deep: {id(original_list_for_deep)}")
print(f"ID of nested list in original for deep: {id(original_list_for_deep[2])}")

deep_copy_list = copy.deepcopy(original_list_for_deep)
print(f"Deep Copy List: {deep_copy_list}")
print(f"ID of deep_copy_list: {id(deep_copy_list)}") # Different ID (new top-level object)
print(f"ID of nested list in deep copy: {id(deep_copy_list[2])}") # Different ID (new nested object)

# Modify deep_copy_list's top-level element
deep_copy_list[0] = 500
print(f"Original List after deep_copy_list top-level change: {original_list_for_deep}")
print(f"Deep Copy List after deep_copy_list top-level change: {deep_copy_list}")
# Original: [1, 2, [6, 7], 8]
# Deep:     [500, 2, [6, 7], 8]
# Top level changed independently.

# Modify deep_copy_list's nested mutable element
deep_copy_list[2].append(77)
print(f"Original List after deep_copy_list nested change: {original_list_for_deep}")
print(f"Deep Copy List after deep_copy_list nested change: {deep_copy_list}")
# Original: [1, 2, [6, 7], 8]
# Deep:     [500, 2, [6, 7, 77], 8]
# Nested list changed ONLY in deep copy. Original remains untouched.

        

Explanation of the Example Code:

  • We start with `original_list` which contains an integer, another integer, a nested *mutable* list `[3, 4]`, and another integer. The `id()` function is used to show the memory address of objects.
  • **Reference Assignment:**
    • `ref_list = original_list` simply makes `ref_list` point to the *same object* in memory as `original_list`. Their `id()`s are identical.
    • When `ref_list[2].append(99)` is executed, it modifies the *shared* nested list. Consequently, printing both `original_list` and `ref_list` shows the same change: `[1, 2, [3, 4, 99], 5]`. This is not a copy at all, but rather a shared reference.
  • **Shallow Copy (`copy.copy()`):**
    • `shallow_copy_list = copy.copy(original_list)` creates a *new top-level list object*. Its `id()` is different from `original_list`.
    • However, the `id()` of the nested list (`shallow_copy_list[2]`) is *the same* as `original_list[2]`. This means the nested list is still shared.
    • When `shallow_copy_list[0] = 100` is executed, only `shallow_copy_list`'s top-level element is changed; `original_list` remains untouched for that element.
    • But when `shallow_copy_list[2].append(88)` is executed, it modifies the *shared nested list*. Therefore, both `original_list` and `shallow_copy_list` reflect this change in their nested structure.
  • **Deep Copy (`copy.deepcopy()`):**
    • We re-initialize `original_list_for_deep` for a clean demonstration.
    • `deep_copy_list = copy.deepcopy(original_list_for_deep)` creates a new top-level list object, and critically, it also creates a *new copy of the nested list*.
    • The `id()` of `deep_copy_list` is different from `original_list_for_deep`, and the `id()` of `deep_copy_list[2]` is also different from `original_list_for_deep[2]`. This indicates true independence.
    • When `deep_copy_list[0] = 500` or `deep_copy_list[2].append(77)` is executed, these modifications **only affect `deep_copy_list`**. The `original_list_for_deep` remains completely unchanged, demonstrating the complete independence provided by a deep copy.

This comprehensive example vividly illustrates the crucial difference between shallow and deep copies, highlighting when to use each based on the desired level of independence for nested mutable objects.