__copy__ / __deepcopy__
__copy__ and __deepcopy__ let you customise how the copy module handles your class. When you implement these dunder methods, copy.copy() and copy.deepcopy() call them instead of using their default strategies.
Method Signatures
__copy__(self) takes no arguments beyond self. It returns the shallow copy of the object.
__deepcopy__(self, memo) receives a memo dictionary that maps object ids to their already-copied counterparts. This dictionary prevents infinite recursion in circular structures. You must pass this same memo dict to any nested deepcopy() calls.
def __copy__(self) -> object:
"""Called by copy.copy(). Returns the shallow copy."""
def __deepcopy__(self, memo: dict[int, object]) -> object:
"""Called by copy.deepcopy(). Receives the shared memo dict."""
How the Copy Module Calls These Methods
When you call copy.copy(obj), the module first checks whether obj has a __copy__ attribute. If it does, it calls obj.__copy__() and returns the result. If not, it falls back to default shallow-copy behaviour, copying the object’s __dict__ and respecting __slots__.
copy.deepcopy(obj) works the same way — it looks for __deepcopy__, calls it with the shared memo dict, and returns the result. If __deepcopy__ is absent, it recursively deep-copies the object’s state instead.
Shallow Copy with copy
Shallow copy creates a new object but leaves nested mutable objects shared between the original and the copy.
import copy
class Stack:
def __init__(self, name: str, items: list = None):
self.name = name
self.items = items if items else []
def __copy__(self) -> "Stack":
# New Stack, same list object — skip __init__ to avoid defensive copy
new = object.__new__(Stack)
new.name = self.name
new.items = self.items # same list reference
return new
stack = Stack("work", ["task1", "task2"])
copied = copy.copy(stack)
copied.items.append("task3")
print(stack.items) # ['task1', 'task2', 'task3'] — shared!
print(copied.items is stack.items) # True
The original’s list was modified because __copy__ returned a new Stack object pointing to the same list.
Deep Copy with deepcopy
Deep copy creates a completely independent copy of the object and everything it contains. The memo dict tracks already-copied objects so the same original object is not copied twice, and so circular references resolve correctly.
import copy
class Stack:
def __init__(self, name: str, items: list = None):
self.name = name
self.items = items if items else []
def __deepcopy__(self, memo: dict[int, object]) -> "Stack":
# New list with deep-copied items — pass memo to nested calls
new_items = copy.deepcopy(self.items, memo)
new = object.__new__(Stack)
new.name = self.name
new.items = new_items
return new
stack = Stack("work", ["task1", "task2"])
deep = copy.deepcopy(stack)
deep.items.append("task4")
print(stack.items) # ['task1', 'task2'] — independent
print(deep.items) # ['task1', 'task2', 'task4']
Handling Circular References
The memo dict is essential when your objects form cycles. You must register the newly created object in memo before recursing into its children — otherwise the deepcopy will loop forever when it encounters a back-edge.
import copy
class Node:
def __init__(self, value: str):
self.value = value
self.children: list["Node"] = []
def add_child(self, child: "Node") -> None:
self.children.append(child)
def __deepcopy__(self, memo: dict[int, object]) -> "Node":
# Return the already-copied version if we've seen this object
if id(self) in memo:
return memo[id(self)]
# Register the new copy BEFORE recursing — prevents infinite loop
new_node = Node(self.value)
memo[id(self)] = new_node
# Pass memo to every nested deepcopy call
new_node.children = [copy.deepcopy(child, memo) for child in self.children]
return new_node
# Build a circular structure
root = Node("root")
child_a = Node("child_a")
child_b = Node("child_b")
root.add_child(child_a)
root.add_child(child_b)
child_a.add_child(root) # back-reference to root
copied = copy.deepcopy(root)
# Verify the cycle is preserved correctly
print(copied.children[0].children[0] is copied) # True
Without __deepcopy__, Python’s default deepcopy would hit a RecursionError on this structure.
Common Mistakes
Forgetting to pass memo to nested deepcopy() calls is the most frequent error:
# WRONG — creates a fresh memo for each call, breaking cycle detection
def __deepcopy__(self, memo):
new_obj = MyClass(self.value)
new_obj.children = [copy.deepcopy(child) for child in self.children]
return new_obj
# CORRECT — thread the same memo through
def __deepcopy__(self, memo):
new_obj = MyClass(self.value)
new_obj.children = [copy.deepcopy(child, memo) for child in self.children]
return new_obj
Registering in memo after recursing also causes infinite loops with circular structures. The registration must happen before any call that might reach back to this object.
Python Version
Both are part of the copy module protocol introduced in Python 2.5 (with __deepcopy__ added in 2.6). Their signatures are unchanged in Python 3.
See Also
- copy module — the module that calls these methods
- pickle module — another way to serialise objects, with its own customisation protocol
- python-copyreg — registering custom reducers for the pickle and copy protocols
- functional-python — immutable data patterns that reduce the need for explicit copying