__copy__ / __deepcopy__

Added in v2.5 · Updated April 1, 2026 · Dunder Methods
copy deepcopy dunder-methods shallow-copy deep-copy

__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