Skip to content

Object Copying & Modification: copy Module

In Python, assignment statements (b = a) do not copy objects; they merely bind a new name to the existing object in memory. When dealing with mutable collections (lists, dictionaries, sets) or complex custom objects, understanding the difference between reference sharing, shallow copying, and deep copying is essential.

The standard library's copy module provides generic shallow and deep copy operations, and starting in Python 3.13, the new copy.replace() function provides a unified mechanism to produce modified copies of objects.


Assignment vs Shallow Copy vs Deep Copy

import copy

original = [[1, 2, 3], [4, 5, 6]]

# 1. Assignment (Alias)
alias = original

# 2. Shallow Copy
shallow = copy.copy(original)  # or original.copy() or list(original)

# 3. Deep Copy
deep = copy.deepcopy(original)

Memory Structure Comparison

1. Assignment (alias = original):
   alias -----\
               +---> [ [1, 2, 3], [4, 5, 6] ]
   original --/

2. Shallow Copy (shallow = copy.copy(original)):
   original ---> [ [1, 2, 3], [4, 5, 6] ]
                      ^          ^
                      |          |  (Inner lists are SHARED)
   shallow  ---> [ ---+----------+ ]

3. Deep Copy (deep = copy.deepcopy(original)):
   original ---> [ [1, 2, 3], [4, 5, 6] ]
   deep     ---> [ [1, 2, 3], [4, 5, 6] ]  (Completely independent in memory)

Behavioral Differences

# Modifying the top-level list container:
original.append([7, 8, 9])
print(len(shallow))  # 2 (shallow copy has its own outer list)
print(len(alias))    # 3 (alias reflects changes to the outer list)

# Modifying a nested mutable element:
original[0][0] = 999
print(shallow[0][0]) # 999 (affected! Inner list was referenced, not copied)
print(deep[0][0])    # 1   (unaffected! Inner list was cloned recursively)

The copy.copy() Function (Shallow Copy)

copy.copy(x) creates a new compound object and inserts references into it to the objects found in the original:

import copy

profile = {
    "user_id": 101,
    "tags": ["python", "backend"],
    "settings": {"dark_mode": True}
}

profile_copy = copy.copy(profile)

# Modifying an immutable top-level value
profile_copy["user_id"] = 202
print(profile["user_id"])  # 101 (original remains untouched)

# Modifying a nested mutable collection
profile_copy["tags"].append("developer")
print(profile["tags"])  # ['python', 'backend', 'developer'] (affected!)

The copy.deepcopy() Function (Deep Copy)

copy.deepcopy(x) creates a new compound object and then, recursively, inserts copies into it of the objects found in the original:

import copy

user_state = {
    "id": 42,
    "permissions": ["read", "write"],
    "sessions": [{"token": "abc", "active": True}]
}

cloned_state = copy.deepcopy(user_state)

# Modify nested objects without impacting the original
cloned_state["permissions"].append("admin")
cloned_state["sessions"][0]["active"] = False

print(user_state["permissions"])         # ['read', 'write']
print(user_state["sessions"][0]["active"]) # True

Handling Circular References

copy.deepcopy() automatically tracks objects it has already copied using an internal memo dictionary. It safely handles self-referential or cyclic data structures without falling into infinite recursion:

import copy

node_a = {"name": "A"}
node_b = {"name": "B"}

# Create circular references
node_a["neighbor"] = node_b
node_b["neighbor"] = node_a

# Deepcopy safely resolves the cycle
copied_a = copy.deepcopy(node_a)
print(copied_a["neighbor"]["neighbor"] is copied_a)  # True

Custom Copy Behavior: __copy__ and __deepcopy__

Classes can define custom copy semantics by implementing the special __copy__() and __deepcopy__(memo) methods:

import copy

class DatabaseConnection:
    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port
        self.is_connected = True

    def __copy__(self):
        # A shallow copy gets the same connection parameters, but disconnected state
        new_conn = DatabaseConnection(self.host, self.port)
        new_conn.is_connected = False
        return new_conn

    def __deepcopy__(self, memo):
        # Re-construct with freshly copied internal attributes
        new_conn = DatabaseConnection(
            copy.deepcopy(self.host, memo),
            copy.deepcopy(self.port, memo)
        )
        new_conn.is_connected = False
        memo[id(self)] = new_conn
        return new_conn

conn1 = DatabaseConnection("localhost", 5432)
conn2 = copy.copy(conn1)

print(conn1.is_connected)  # True
print(conn2.is_connected)  # False

Python 3.13: copy.replace() (PEP 698)

Python 3.13 introduced copy.replace(), a standardized function to create a copy of an object with specified attributes replaced.

It operates on any object implementing the __replace__() method, including dataclasses and namedtuples out of the box:

from dataclasses import dataclass
import copy

@dataclass(frozen=True)
class ServerConfig:
    host: str
    port: int
    debug: bool = False
    timeout: int = 30

# Original immutable configuration
prod_config = ServerConfig(host="api.production.internal", port=443, debug=False)

# Create a modified clone for local testing using copy.replace()
test_config = copy.replace(prod_config, host="127.0.0.1", port=8000, debug=True)

print("Original:", prod_config)
print("Modified:", test_config)

Output:

Original: ServerConfig(host='api.production.internal', port=443, debug=False, timeout=30)
Modified: ServerConfig(host='127.0.0.1', port=8000, debug=True, timeout=30)

Implementing __replace__() in Custom Classes

You can make your own classes compatible with copy.replace():

import copy

class RGBColor:
    def __init__(self, r: int, g: int, b: int):
        self.r = r
        self.g = g
        self.b = b

    def __replace__(self, **changes):
        new_values = {
            "r": self.r,
            "g": self.g,
            "b": self.b,
            **changes
        }
        return RGBColor(**new_values)

    def __repr__(self):
        return f"RGBColor({self.r}, {self.g}, {self.b})"

c1 = RGBColor(255, 0, 0)
c2 = copy.replace(c1, g=128)
print(c2)  # RGBColor(255, 128, 0)

Summary Comparison

Mechanism Method Nested Mutable Sharing Use Case
Assignment b = a Shared Renaming / passing references.
Shallow Copy copy.copy(a) or a.copy() Shared Duplicate outer container only.
Deep Copy copy.deepcopy(a) Independent Completely isolated copy of nested structures.
Replacement Copy copy.replace(a, ...) (3.13+) Custom Immutable dataclasses and records with modified fields.