python
copy
deepcopy
object-oriented-programming
python-tutorial

How to override the copy/deepcopy operations for a Python object?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Python copy and deepcopy are useful defaults, but custom objects often need explicit behavior for shared resources, caches, or immutable fields. You can control this by implementing __copy__ and __deepcopy__. Correct overrides prevent accidental state sharing and expensive duplication.

Default Copy Behavior Recap

copy.copy performs shallow copy. Nested mutable objects remain shared.

python
1import copy
2
3obj = {"a": [1, 2]}
4shallow = copy.copy(obj)
5deep = copy.deepcopy(obj)
6
7obj["a"].append(3)
8print(shallow)  # list changed
9print(deep)     # independent list

For custom classes, defaults may not align with your intent.

Implementing __copy__

Use __copy__ when shallow copy must copy selected fields and share others.

python
1import copy
2
3class Config:
4    def __init__(self, name, cache):
5        self.name = name
6        self.cache = cache
7
8    def __copy__(self):
9        cls = self.__class__
10        result = cls.__new__(cls)
11        result.name = self.name
12        result.cache = self.cache
13        return result
14
15c1 = Config("job", {"k": 1})
16c2 = copy.copy(c1)
17print(c1 is c2)
18print(c1.cache is c2.cache)

This gives full control over what is reused.

Implementing __deepcopy__

Deep copy should recursively copy mutable state and honor memoization.

python
1import copy
2
3class Node:
4    def __init__(self, value, children=None):
5        self.value = value
6        self.children = children or []
7
8    def __deepcopy__(self, memo):
9        cls = self.__class__
10        result = cls.__new__(cls)
11        memo[id(self)] = result
12
13        result.value = copy.deepcopy(self.value, memo)
14        result.children = copy.deepcopy(self.children, memo)
15        return result
16
17root = Node("r", [Node("a"), Node("b")])
18clone = copy.deepcopy(root)
19print(root is clone)
20print(root.children[0] is clone.children[0])

Always use memo to avoid infinite recursion in cyclic graphs.

Managing Non-Copyable Resources

File handles, sockets, locks, and database connections usually should not be duplicated blindly.

python
1class Worker:
2    def __init__(self, name, connection, settings):
3        self.name = name
4        self.connection = connection
5        self.settings = settings
6
7    def __deepcopy__(self, memo):
8        cls = self.__class__
9        result = cls.__new__(cls)
10        memo[id(self)] = result
11
12        result.name = self.name
13        result.connection = self.connection  # intentionally shared
14        result.settings = copy.deepcopy(self.settings, memo)
15        return result

Document shared resource decisions so callers do not assume full isolation.

Dataclasses and Copying

Dataclasses do not automatically solve deep copy semantics. They work with copy module, but custom behavior is still needed for complex fields.

python
1from dataclasses import dataclass, field
2
3@dataclass
4class Job:
5    name: str
6    tags: list[str] = field(default_factory=list)

For simple data models, defaults are fine. For graph-like models, explicit copy methods are safer.

Testing Copy Semantics

Write tests for identity and independence expectations.

python
1def test_deepcopy_independent_children():
2    import copy
3    n1 = Node("x", [Node("y")])
4    n2 = copy.deepcopy(n1)
5    assert n1 is not n2
6    assert n1.children[0] is not n2.children[0]

These tests prevent subtle regressions during class evolution.

Integration with Pickle and Serialization

Custom copy behavior should align with object serialization behavior when your application persists state or sends objects between processes. If copy and serialization disagree about which fields are shared or cloned, bugs can appear only in production workflows. Review __getstate__ and related hooks alongside copy methods when designing object lifecycle rules.

Treat copy behavior as part of class public contract and document it clearly.

In larger systems, adding debug assertions inside copy methods can catch broken invariants early. For example, assert that required identifiers remain non-empty and that copied containers have expected sizes. Defensive checks are valuable when copy rules evolve over time.

Common Pitfalls

  • Implementing __deepcopy__ without using memo, causing recursion errors on cyclic references.
  • Deep-copying non-copyable resources such as sockets and locks.
  • Assuming shallow copy duplicates nested mutable fields.
  • Forgetting to copy newly added attributes in custom copy methods.
  • Returning partially initialized objects from __copy__ or __deepcopy__.

Summary

  • Override __copy__ and __deepcopy__ when default behavior does not match object semantics.
  • Use memo in deep copy implementations to handle object graphs safely.
  • Share or clone resources intentionally and document the policy.
  • Add tests that verify both identity and deep independence rules.
  • Revisit copy logic whenever class fields change.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.