Assigning a list, dictionary, or class instance to another variable does not automatically create a new object. In Python, assignment usually creates another name that points to the same value. This is why a change made through one variable can unexpectedly appear through another. The Python copy module provides explicit tools for shallow copies, deep copies, and field replacement in immutable-style objects.
This guide explains identity versus equality, copy.copy(), copy.deepcopy(), the internal memo dictionary, custom copying through __copy__() and __deepcopy__(), and the newer copy.replace() API. It complements the guides about Python lists, Python slicing, equality versus identity, Python dictionaries, and Python descriptors.
Assignment is not copying
original = [1, 2, 3]
alias = original
alias.append(4)
print(original) # [1, 2, 3, 4]
print(alias is original) # TrueBoth names point to the same list. The append() call mutates the shared object. The is operator confirms identity: the variables refer to exactly the same object.
This behavior is efficient and usually desirable. A separate copy is needed only when the program requires independent modification.
Shallow copies with copy.copy()
copy.copy() creates a new compound object but reuses references to the objects stored inside it. This is called a shallow copy.
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
print(shallow is original) # False
print(shallow[0] is original[0]) # TrueThe outer list is new, while the inner lists remain shared. Appending a new row to the copy does not affect the source. Mutating an existing inner row affects both.
shallow.append([5, 6])
shallow[0].append(99)
print(original) # [[1, 2, 99], [3, 4]]
print(shallow) # [[1, 2, 99], [3, 4], [5, 6]]Built-in copying methods
Lists, dictionaries, and sets expose convenient methods:
new_list = old_list.copy()
new_dict = old_dict.copy()
new_set = old_set.copy()A full slice also creates a shallow list or sequence copy:
new = old[:]These forms are clear when a function works with a known built-in type. copy.copy() is useful for generic code and custom instances. The official Python copy documentation also notes that some native methods and slicing operations may create a base-type instance when applied to a subclass, while copy.copy() normally preserves the concrete type.
Deep copies with copy.deepcopy()
copy.deepcopy() recursively traverses a compound object and copies its components. This prevents accidental sharing of nested mutable values.
import copy
original = {
"user": "Ana",
"permissions": ["read", "edit"],
"preferences": {"theme": "dark"},
}
deep = copy.deepcopy(original)
deep["permissions"].append("publish")
deep["preferences"]["theme"] = "light"
print(original["permissions"])
print(original["preferences"])The nested list and dictionary are independent, so modifying the copy does not alter the original.
Deepcopy does not duplicate everything
A deep copy does not guarantee a new identity for every value. Immutable objects, functions, and classes can be reused safely. Modules, open files, sockets, stack frames, windows, and similar runtime resources are not copied generically.
Recursive copying may also copy too much. Connections, locks, shared services, registries, and global caches often should remain shared. Use deepcopy() only when recursive independence is part of the object’s intended contract.
Recursive references
Objects can refer to themselves:
items = []
items.append(items)A naive recursive copier would enter an infinite loop. deepcopy() prevents this by keeping an internal dictionary named memo. It records objects that have already been copied during the current operation.
import copy
items = []
items.append(items)
cloned = copy.deepcopy(items)
print(cloned is cloned[0]) # True
print(cloned is items) # FalseThe self-reference is preserved without reusing the original list.
Why memo matters
The memo dictionary also preserves internal sharing. When two paths in the source graph point to the same object, the copied graph normally points both paths to the same cloned object rather than creating inconsistent duplicates.
import copy
settings = {"limit": 10}
original = [settings, settings]
cloned = copy.deepcopy(original)
print(cloned[0] is cloned[1]) # True
print(cloned[0] is settings) # FalseThis behavior is essential for object graphs in which identity relationships carry meaning.
Deep copying can copy too much
Consider a domain object that contains business data and a shared external service:
class Order:
def __init__(self, items, gateway):
self.items = items
self.gateway = gatewayThe item data may need an independent copy, while the payment gateway must remain shared. Automatic deep copying may not express this rule correctly. Custom copying solves the problem.
Customizing __copy__()
A class can define __copy__() to control shallow-copy behavior:
import copy
class Document:
def __init__(self, title, metadata):
self.title = title
self.metadata = metadata
def __copy__(self):
return type(self)(self.title, self.metadata)
document = Document("Report", {"version": 1})
new_document = copy.copy(document)
print(new_document is document)
print(new_document.metadata is document.metadata)The document instance is new, while its metadata remains shared, matching shallow semantics.
Customizing __deepcopy__()
__deepcopy__(self, memo) receives the memoization dictionary. Treat it as opaque and pass it to recursive calls of copy.deepcopy().
import copy
class Order:
def __init__(self, items, gateway):
self.items = items
self.gateway = gateway
def __deepcopy__(self, memo):
cloned = type(self)(
copy.deepcopy(self.items, memo),
self.gateway,
)
memo[id(self)] = cloned
return clonedThe item collection becomes independent, but the service remains shared. Registering the new instance in memo is particularly important for cycles and repeated references.
Immutable updates with copy.replace()
Added in Python 3.13, copy.replace() creates a new object of the same type while replacing selected fields. It is convenient for immutable-style records.
from copy import replace
from dataclasses import dataclass
@dataclass(frozen=True)
class Product:
name: str
price: float
stock: int
original = Product("Keyboard", 120.0, 15)
discounted = replace(original, price=99.9)
print(original)
print(discounted)The source object remains unchanged. The API supports dataclasses, named tuples, and custom classes implementing __replace__().
copy.replace() is not deepcopy()
replace() replaces named fields and reuses all other values. A mutable field that is not replaced remains shared.
from copy import replace
from dataclasses import dataclass
@dataclass(frozen=True)
class Plan:
name: str
steps: list[str]
base = Plan("Launch", ["test", "production"])
new = replace(base, name="Launch v2")
print(new.steps is base.steps) # TrueUse replace() for functional updates to records, not as a synonym for recursive independence.
Implementing __replace__()
Custom classes can participate in the protocol:
class Coordinate:
def __init__(self, x, y):
self.x = x
self.y = y
def __replace__(self, **changes):
allowed = {"x", "y"}
unknown = set(changes) - allowed
if unknown:
raise TypeError(f"Invalid fields: {unknown}")
return type(self)(
changes.get("x", self.x),
changes.get("y", self.y),
)After this method is available, copy.replace(coordinate, x=10) creates a new instance with a controlled update.
Nested dictionaries and shallow-copy traps
defaults = {
"api": {"timeout": 10},
"logs": {"level": "INFO"},
}
production = defaults.copy()
production["api"]["timeout"] = 30
print(defaults["api"]["timeout"]) # 30The outer dictionary is copied, but the nested dictionaries are shared. Independent configuration trees require explicit reconstruction, a controlled merge, or a safe deep copy.
Copying lists of objects
Copying a list does not copy the stored instances:
users_copy = users.copy()
print(users_copy is users) # False
print(users_copy[0] is users[0]) # TrueThis is useful when the new list represents another ordering or filtered view of the same objects. If every user must be independent, use a domain-specific clone operation or a carefully designed deep copy.
When explicit reconstruction is better
Domain models often benefit from constructing the new object explicitly. The code shows which fields are cloned, recalculated, or intentionally shared.
new_order = Order(
items=[item.clone() for item in order.items],
gateway=order.gateway,
)This approach is more verbose but can communicate ownership more clearly than a generic recursive copy.
Performance and memory
A shallow copy generally costs time proportional to the number of references in the outer container. A deep copy traverses the reachable object graph and may consume substantially more CPU and memory.
Avoid copying large structures repeatedly without measurement. Immutable values, structural sharing, field replacement, generators, and persistent storage may be better alternatives. The official Python data structures tutorial provides useful context about list, dictionary, set, and sequence operations.
Testing the intended independence
Do not test only equality. Verify identity and later mutations:
import copy
original = {"items": [[1], [2]]}
cloned = copy.deepcopy(original)
assert cloned == original
assert cloned is not original
assert cloned["items"] is not original["items"]
assert cloned["items"][0] is not original["items"][0]
cloned["items"][0].append(9)
assert original["items"][0] == [1]Custom classes should also be tested with cycles, repeated references, shared resources, and subclasses.
Common mistakes
- Assuming
b = acreates a new object. - Using
list.copy()while expecting nested elements to be duplicated. - Applying
deepcopy()blindly to sockets, locks, connections, or services. - Destroying meaningful identity relationships inside an object graph.
- Implementing
__deepcopy__()without forwardingmemo. - Treating
copy.replace()as a deep copy. - Duplicating large structures without measuring cost.
- Copying when explicit reconstruction would be clearer.
Best practices
- Define the required level of independence first.
- Use shallow copies when only the outer container should change.
- Use deep copies when all reachable mutable state must be independent.
- Customize classes that own shared runtime resources.
- Use
copy.replace()for functional updates to compatible records. - Document which components remain shared.
- Test equality, identity, and mutations after copying.
- Benchmark time and memory on large object graphs.
Conclusion
The Python copy module addresses a fundamental distinction: creating another name, copying only the outer object, and recursively duplicating an object graph are different operations. copy.copy() creates a shallow copy. copy.deepcopy() uses memoization to handle cycles and preserve internal sharing. copy.replace() creates updated versions of compatible records by replacing selected fields.
The right choice depends on ownership. Shallow copies are efficient and intentionally share components. Deep copies provide independence but may duplicate too much. Custom protocols combine both strategies when domain objects contain mixed responsibilities. Making this decision explicit prevents surprising mutations, unnecessary memory use, and incorrect duplication of runtime resources.







