Python

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.

August 2026 4 min read 14 views 0 hearts

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:

  1. Your class has non-copyable attributes (file handles, network connections)
  2. You need fine-grained control over which attributes get cloned
  3. Performance matters — deep copying large objects can be expensive
  4. 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.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.