The Fundamental Differences Between Shallow Copy, Deep Copy, and Assignment Operations in Python

Dec 08, 2025 · Programming · 11 views · 7.8

Keywords: Python | Shallow Copy | Deep Copy | Assignment Operation | Mutable Objects | Immutable Objects | Memory Management | Object Reference

Abstract: This article provides an in-depth exploration of the core distinctions between shallow copy (copy.copy), deep copy (copy.deepcopy), and normal assignment operations in Python programming. By analyzing the behavioral characteristics of mutable and immutable objects with concrete code examples, it explains the different implementation mechanisms in memory management, object referencing, and recursive copying. The paper focuses particularly on compound objects (such as nested lists and dictionaries), revealing that shallow copies only duplicate top-level references while deep copies recursively duplicate all sub-objects, offering theoretical foundations and practical guidance for developers to choose appropriate copying strategies.

Basic Concepts of Object Copying Mechanisms in Python

In Python programming, object copying is one of the fundamental operations in memory management and data manipulation. Understanding the differences between various copying methods is crucial for avoiding unintended data modifications and achieving efficient memory usage. Python provides three main copying mechanisms: normal assignment operations, shallow copy, and deep copy, each exhibiting distinct behavioral characteristics in handling object references and memory allocation.

Behavioral Differences in Copying Mutable vs Immutable Objects

Objects in Python can be categorized as immutable (e.g., strings, tuples, integers) or mutable (e.g., lists, dictionaries, sets) based on their mutability. This classification directly influences the behavior of copying operations:

import copy

# Immutable object examples
str_obj = "Python"
tuple_obj = (1, 2, 3)

# Mutable object examples
list_obj = [1, 2, 3]
dict_obj = {"a": 1, "b": 2}

# Shallow copy operations
str_shallow = copy.copy(str_obj)
tuple_shallow = copy.copy(tuple_obj)
list_shallow = copy.copy(list_obj)
dict_shallow = copy.copy(dict_obj)

print(f"String ID comparison: {id(str_obj) == id(str_shallow)}")  # Output: True
print(f"Tuple ID comparison: {id(tuple_obj) == id(tuple_shallow)}")  # Output: True
print(f"List ID comparison: {id(list_obj) == id(list_shallow)}")  # Output: False
print(f"Dictionary ID comparison: {id(dict_obj) == id(dict_shallow)}")  # Output: False

From the above code, we can observe that for immutable objects, shallow copy actually returns a reference to the original object because Python optimizes memory usage for immutable objects—objects with the same value typically share memory. For mutable objects, shallow copy creates a new container object, but the elements within the container still reference the elements from the original object.

The Nature of Normal Assignment Operations

Normal assignment operations (using the equals sign =) in Python do not create new objects but rather create new variable names pointing to the same memory object. This means that regardless of whether the object is mutable or immutable, after assignment, both variable names reference exactly the same object:

# Assignment operation example
original_list = [1, 2, 3]
assigned_list = original_list

print(f"List IDs are identical: {id(original_list) == id(assigned_list)}")  # Output: True

# Modifying the variable obtained through assignment affects the original object
assigned_list.append(4)
print(f"Original list is also modified: {original_list}")  # Output: [1, 2, 3, 4]

The core principle of this behavior is that Python variable names are merely reference labels to objects. Assignment operations simply add new labels pointing to the same object without creating a copy of the object.

Mechanisms and Limitations of Shallow Copy

Shallow copy is implemented through the copy.copy() function, which creates a new container object but only copies references to the elements within the container, not the elements themselves. This is effective for one-dimensional mutable objects but may produce unexpected results for nested compound objects:

# Shallow copy behavior with compound objects
import copy

nested_list = [[1, 2], [3, 4]]
shallow_copied = copy.copy(nested_list)

print(f"Outer list IDs differ: {id(nested_list) == id(shallow_copied)}")  # Output: False
print(f"Inner first list IDs are identical: {id(nested_list[0]) == id(shallow_copied[0])}")  # Output: True

# Modifying inner elements of the shallow-copied object affects the original object
shallow_copied[0].append(5)
print(f"Original nested list is modified: {nested_list}")  # Output: [[1, 2, 5], [3, 4]]

This characteristic of shallow copy makes it suitable for scenarios requiring independent outer containers but shared inner data. However, developers must be aware of the cascading effects of modifying inner data.

Recursive Copying Mechanism of Deep Copy

Deep copy is implemented through the copy.deepcopy() function, which recursively copies the object and all its sub-objects, creating a completely independent object hierarchy. This is the only copying method among the three that ensures complete isolation between the original object and the copied object:

# Deep copy behavior with compound objects
import copy

complex_structure = {
    "list": [1, 2, 3],
    "dict": {"inner": [4, 5]},
    "nested": [[6, 7], [8, 9]]
}

deep_copied = copy.deepcopy(complex_structure)

# Verify that new objects are created at all levels
print(f"Outer dictionary IDs differ: {id(complex_structure) == id(deep_copied)}")  # Output: False
print(f"Inner list IDs differ: {id(complex_structure['list']) == id(deep_copied['list'])}")  # Output: False
print(f"Deeply nested list IDs differ: {id(complex_structure['nested'][0]) == id(deep_copied['nested'][0])}")  # Output: False

# Modifying the deep-copied object does not affect the original object
deep_copied["list"].append(10)
deep_copied["dict"]["inner"].append(11)
print(f"Original structure remains unchanged: {complex_structure['list']}")  # Output: [1, 2, 3]
print(f"Original structure remains unchanged: {complex_structure['dict']['inner']}")  # Output: [4, 5]

This complete independence of deep copy makes it the ideal choice when fully isolated data copies are needed, particularly in multi-threaded environments or scenarios requiring immutable original data.

Practical Application Scenarios and Selection Guidelines

In practical development, appropriate copying strategies should be selected based on specific requirements:

  1. Normal Assignment: Suitable for scenarios requiring multiple variable names to reference the same object, such as function parameter passing, temporary variable renaming, etc. However, note that modifications will synchronously affect all references.
  2. Shallow Copy: Suitable for scenarios requiring independent outer containers but shared inner data, such as creating configuration templates, data views, etc. When inner data consists of immutable objects, shallow copy achieves the same effect as deep copy but with better performance.
  3. Deep Copy: Suitable for scenarios requiring completely independent data copies, such as data backup, experimental data isolation, concurrent data processing, etc. Although it incurs greater performance overhead, it ensures secure data isolation.

Particular attention should be paid to compound objects containing custom class instances. Deep copy will recursively call the class's __deepcopy__ method (if defined), otherwise using the default copying mechanism. This provides flexibility in controlling the copying behavior of complex objects.

Performance Considerations and Best Practices

From a performance perspective, normal assignment operations are the fastest (O(1) time complexity), shallow copy is next, and deep copy is the slowest (depending on the complexity of the object structure). When handling large data structures, deep copy should be used cautiously to avoid unnecessary performance overhead.

Best practice recommendations include:

By deeply understanding Python's copying mechanisms, developers can write more efficient, secure, and maintainable code, avoiding subtle errors caused by object reference issues.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.