When and How to Implement __copy__ and __deepcopy__ in Python
Learn when and how to override Python's __copy__ and __deepcopy__ methods to control object cloning in custom classes, avoid shared state bugs, and handle edge cases like circular references and resources.
You're Probably Cloning Python Objects Wrong. Here's What Works.
Let me paint you a picture. You're working on a data processing pipeline at PythonSkillset, and you need to duplicate a complex object without affecting the original. You write new_item = original_item, make some changes, and suddenly your original data is corrupted. Sound familiar?
This is where Python's __copy__ and __deepcopy__ methods come in. They're not just fancy syntax — they're your safety net when dealing with mutable objects.
The Shallow vs Deep Problem
Here's the core issue. When you have nested objects in Python — like a list of dictionaries — a regular assignment only copies the references, not the actual objects. A shallow copy (copy.copy()) creates a new container but fills it with references to the same nested objects. A deep copy (copy.deepcopy()) creates entirely independent copies at every level.
Let's see this in action with a real PythonSkillset scenario:
import copy
class DataPipeline:
def __init__(self, config, records):
self.config = config # dictionary
self.records = records # list of dictionaries
def __copy__(self):
# Custom shallow copy behavior
return DataPipeline(
config=self.config, # shares reference
records=self.records # shares reference
)
def __deepcopy__(self, memo):
# Custom deep copy behavior
return DataPipeline(
config=copy.deepcopy(self.config, memo),
records=copy.deepcopy(self.records, memo)
)
# Our original data
original = DataPipeline(
config={"batch_size": 100, "format": "csv"},
records=[{"id": 1, "value": "A"}, {"id": 2, "value": "B"}]
)
# The problem: shallow copy
shallow_copy = copy.copy(original)
shallow_copy.config["batch_size"] = 200
print(original.config["batch_size"]) # Output: 200! Contamination!
# The solution: deep copy
deep_copy = copy.deepcopy(original)
deep_copy.config["batch_size"] = 300
print(original.config["batch_size"]) # Output: 200! Safe!
When Default Behavior Isn't Enough
Python's default copy.copy() and copy.deepcopy() work for most built-in types. But custom classes need explicit support, especially when you have:
- Cached or computed attributes that shouldn't be copied
- External resources like database connections or file handles
- Circular references (objects referencing each other)
- Singleton objects where you want to maintain shared state
Here's a practical example from PythonSkillset's data handling:
import copy
import hashlib
class SecureRecord:
def __init__(self, data, connection_string):
self.data = data
self.connection = connection_string # Don't copy this
self._hash_cache = None
def __copy__(self):
# Shallow copy but recreate connection
new_record = SecureRecord(
data=self.data,
connection_string="new_connection" # Fresh connection
)
return new_record
def __deepcopy__(self, memo):
# Deep copy all data but recreate connection
new_record = SecureRecord(
data=copy.deepcopy(self.data, memo),
connection_string="new_connection"
)
return new_record
@property
def hash(self):
if self._hash_cache is None:
self._hash_cache = hashlib.sha256(str(self.data).encode()).hexdigest()
return self._hash_cache
# Usage
original = SecureRecord({"user": "PythonSkillset", "score": 95}, "db://production")
cloned = copy.deepcopy(original)
# Both work independently
original.data["score"] = 100
print(cloned.data["score"]) # Output: 95 — independent
The Memo Dictionary: Your Circular Reference Solution
One thing that trips up many developers is circular references. Your objects might point to each other, and a naive deep copy would loop forever. Python's deepcopy uses a memo dictionary to track already-copied objects. When implementing __deepcopy__, you must pass this memo down:
class Parent:
def __init__(self):
self.child = None
def __deepcopy__(self, memo):
new_parent = Parent()
memo[id(self)] = new_parent # Register before copying child
if self.child is not None:
new_parent.child = copy.deepcopy(self.child, memo)
return new_parent
class Child:
def __init__(self, parent):
self.parent = parent
def __deepcopy__(self, memo):
new_child = Child(parent=None) # Temporary
memo[id(self)] = new_child
new_child.parent = copy.deepcopy(self.parent, memo)
return new_child
When to Implement These Methods
You don't need __copy__ and __deepcopy__ for every class. Only implement them when:
- Your class has non-copyable attributes (file handles, network connections)
- You need fine-grained control over which attributes get cloned
- Performance matters — deep copying large objects can be expensive
- You have invariants that must be maintained after cloning
The Bottom Line
Python's __copy__ and __deepcopy__ give you precise control over how your objects clone themselves. Without them, you risk nasty bugs from shared state. With them, you get predictable, safe object duplication.
Next time you're building a PythonSkillset pipeline or any system where data integrity matters, think about your object copying strategy. Your future self — and your coworkers — will thank you.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.