Back to Blog
Python

Implementing a Custom Deepcopy in Python

python custom deepcopy: Learn how to implement __deepcopy__ in Python to control how copy.deepcopy treats your objects, including memo handling, resources, and circula...

deepcopycopy module__deepcopy__object copyingpython classes
Illustration of a Python object being duplicated with a custom deepcopy method controlling which internal resources are copied.

python custom deepcopy requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

What deepcopy Does by Default

copy.deepcopy recursively copies an object and every object it references. For a plain class instance, that means copying the __dict__ (or __slots__ values) and then recursing into each attribute. The result is a fully independent object graph: mutating a nested attribute on the copy does not affect the original.

The default behavior works well for data-oriented classes. It fails when an object holds resources that cannot be copied by value — a file handle, a database connection, a thread, a lock, or a reference to a singleton. It also fails when you want the copy to share certain attributes while duplicating others.

This is where a python custom deepcopy becomes necessary. By implementing __deepcopy__ on your class, you take control of what copy.deepcopy does with instances of that class.

When the Default Behavior Fails

Consider a class that opens a file in its constructor:

class LogWriter: def __init__(self, path): self.path = path self._handle = open(path, "a")

Calling copy.deepcopy on an instance of this class will attempt to copy the open file object. File objects are not copyable, so deepcopy raises a TypeError:

TypeError: cannot pickle '_io.TextIOWrapper' object

The exact exception depends on the Python version and the type of the underlying resource, but the problem is the same: deepcopy tries to duplicate something that cannot be duplicated.

A custom __deepcopy__ lets you decide what happens instead. In this case, the copy should open a new file handle to the same path, or share the existing handle, depending on the semantics you want.

Implementing __deepcopy__

The method signature is fixed:

def __deepcopy__(self, memo): ...

memo is a dictionary that copy.deepcopy uses to track objects that have already been copied. It maps id(original) to the corresponding copy. Your implementation must accept it and pass it through to any recursive copy.deepcopy calls you make.

The method must return the new object. A minimal implementation for a simple class looks like this:

import copy class Config: def __init__(self, values): self.values = values def __deepcopy__(self, memo): new = Config(copy.deepcopy(self.values, memo)) memo[id(self)] = new return new

The order matters. The new object is registered in memo before any recursive copying happens. This is what prevents infinite recursion when the object graph contains cycles.

Handling the memo Parameter Correctly

The memo parameter is the part of __deepcopy__ that is easiest to get wrong. If you ignore it, you break two guarantees that copy.deepcopy normally provides.

The first guarantee is identity preservation. If the same object is referenced from two places in the graph, deepcopy should produce a single copy that both references point to. Without registering in memo, each recursive call would create a separate copy, and the copied graph would no longer mirror the original structure.

The second guarantee is termination. For a circular structure, such as a tree where a child holds a reference to its parent, an unregistered copy will recurse forever:

class Node: def __init__(self): self.parent = None self.children = [] def __deepcopy__(self, memo): new = Node() # Register before recursing into attributes. memo[id(self)] = new new.parent = copy.deepcopy(self.parent, memo) new.children = copy.deepcopy(self.children, memo) return new

If the memo[id(self)] = new line is moved after the recursive calls, the copy of self.parent will eventually hit the original self again and start a new copy, which will again copy its parent, and so on until the recursion limit is reached.

The rule is simple: register the new object in memo immediately after creating it, before copying any attribute that could reference the original object.

A Practical Example: Copying an Object With a Resource

A common real-world case is a class that holds both plain data and a resource that should not be duplicated. The custom __deepcopy__ copies the data and recreates the resource:

import copy class Session: def __init__(self, user_id, connection=None): self.user_id = user_id self.connection = connection def connect(self, connection): self.connection = connection def __deepcopy__(self, memo): # The connection is intentionally shared, not copied. new = Session(self.user_id, self.connection) memo[id(self)] = new return new

Here the copy shares the connection object while duplicating the user_id value. If the connection should be re-established rather than shared, you would create a new connection inside __deepcopy__ instead of passing the old one through.

The decision of what to copy and what to share is the entire point of writing a custom implementation. There is no general rule that fits every class; the method exists precisely so that the class author can encode that decision.

Performance and Identity Considerations

A custom __deepcopy__ can also be a performance optimization, though that should not be the primary reason to write one. When an object contains a large attribute that is known to be immutable, such as a frozen configuration or a cached compiled pattern, copying it with copy.deepcopy wastes time and memory. Returning it directly from __deepcopy__ avoids that cost:

class CompiledQuery: def __init__(self, source, pattern): self.source = source self._compiled = re.compile(pattern) def __deepcopy__(self, memo): # The compiled pattern is immutable and safe to share. new = CompiledQuery(self.source, self._compiled.pattern) memo[id(self)] = new return new

The compiled regex is recreated in the new instance, but the underlying pattern string is shared. If the compiled object itself were stored and returned directly, both instances would reference the same compiled pattern, which is usually acceptable because compiled patterns are immutable.

The tradeoff is maintainability. A custom __deepcopy__ must be kept in sync with the class's fields. When a new attribute is added to the class, the method must be updated to handle it; otherwise, the new attribute is silently omitted from copies. This is a real cost, and it means you should only write __deepcopy__ when the default behavior is genuinely wrong for the class.

Common Mistakes and Edge Cases

The most frequent mistake is forgetting to register the new object in memo before recursing. The result is either infinite recursion or duplicated objects in the copied graph, depending on whether the graph contains cycles.

Another mistake is returning the original object from __deepcopy__. For an immutable class this can be correct, but for a mutable class it breaks the contract of deepcopy: mutating the "copy" would mutate the original. If you intend to share an attribute, share it inside a new instance; do not return the original instance itself.

A subtler issue is __slots__. If your class uses __slots__ instead of __dict__, you must copy the slot values manually, because there is no __dict__ to copy:

class Point: __slots__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y def __deepcopy__(self, memo): new = Point(copy.deepcopy(self.x, memo), copy.deepcopy(self.y, memo)) memo[id(self)] = new return new

Finally, be aware that __deepcopy__ is only used by copy.deepcopy. The pickle module and the copy.copy function use different protocols. If your class also needs to support pickling, __deepcopy__ alone is not enough.

Choosing Between __deepcopy__, __copy__, and __reduce__

copy.copy uses __copy__ when it is defined. It produces a shallow copy, duplicating the object itself but sharing the referenced objects. If a class needs both shallow and deep copy behavior, implement both methods.

__reduce__ is used by pickle and also by copy.deepcopy as a fallback when __deepcopy__ is not defined. If you already implement __reduce__ for pickling, deepcopy will use it, and the result may or may not match what you want. For example, a __reduce__ that reconstructs the object from a database ID will produce a deepcopy that shares nothing with the original, which is usually fine, but it also means the copy is not created from the original's current in-memory state.

The practical guidance is: implement __deepcopy__ when you need precise control over how copy.deepcopy treats your class. Implement __copy__ when shallow copy behavior also needs to be customized. Rely on __reduce__ only when pickling is the primary requirement and deepcopy behavior is acceptable as a side effect.

python custom deepcopy: Practical Usage and Code Examples | RYUSLOG DEV