Skip to main content

Lib/copy.py

cpython 3.14 @ ab2d84fe1023/Lib/copy.py

A short, self-contained module that provides two public functions: copy for shallow copies and deepcopy for recursive deep copies. Neither function has a C accelerator; the entire implementation is pure Python.

The module's design predates the modern __copy__ / __deepcopy__ protocol and still carries the older copyreg.dispatch_table path for compatibility with pickled objects. The two dispatch chains overlap but are separate: copy has no recursion or memo dict, while deepcopy carries a memo dict throughout to handle cyclic object graphs and shared references.

Atomic types (integers, floats, booleans, strings, bytes, None, Ellipsis, NotImplemented, and types themselves) are returned unchanged by both functions because they are immutable and identity is safe to share. For mutable containers (list, dict, set) deepcopy recurses into every element before constructing the new container, then stores the result in memo before returning it so that self-referential structures terminate.

Map

LinesSymbolRolegopy
1-60Module header, _copy_dispatch, ErrorImports, the atomic-type fast-path dispatch table for copy, and the module-level Error exception alias.module/copy/module.go
61-130copyShallow copy; dispatches through _copy_dispatch, then copyreg.dispatch_table, then __copy__, then falls back to __reduce_ex__.module/copy/module.go
131-240deepcopy, _deepcopy_atomic, _deepcopy_list, _deepcopy_tuple, _deepcopy_dictDeep copy entry point and per-type helpers; memo maps id(obj) to the already-copied object to break cycles.module/copy/module.go
241-270_reconstructRebuilds objects from the five-tuple returned by __reduce__ / __reduce_ex__; handles state dicts, list items, and dict items.module/copy/module.go
271-300_deepcopy_dispatch population, replaceFills the _deepcopy_dispatch table for all atomic and built-in container types; replace is a 3.13+ addition for copy.replace.module/copy/module.go

Reading

copy dispatch chain (lines 61 to 130)

cpython 3.14 @ ab2d84fe1023/Lib/copy.py#L61-130

def copy(x):
cls = type(x)

copier = _copy_dispatch.get(cls)
if copier:
return copier(x)

if issubclass(cls, type):
# treat it as a regular class
return _copy_immutable(x)

copier = getattr(cls, "__copy__", None)
if copier is not None:
return copier()

reductor = getattr(cls, "__reduce_ex__", None)
rv = reductor(4)
if isinstance(rv, str):
return x
return _reconstruct(x, None, *rv)

The dispatch order is:

  1. _copy_dispatch table: covers the atomic types (int, float, str, bytes, bool, None, type, etc.) and returns the object unchanged.
  2. issubclass(cls, type): class objects are immutable, returned as-is.
  3. cls.__copy__(): the recommended hook for custom shallow-copy logic.
  4. __reduce_ex__(4): the pickle protocol 4 fallback; if the result is a string the object is a global and is returned unchanged, otherwise _reconstruct reassembles it from the reduce tuple.

Note that copyreg.dispatch_table is checked inside _reconstruct rather than at the top of copy, unlike deepcopy which checks it earlier.

deepcopy and the memo dict (lines 131 to 240)

cpython 3.14 @ ab2d84fe1023/Lib/copy.py#L131-240

def deepcopy(x, memo=None, _nil=[]):
if memo is None:
memo = {}

d = id(x)
y = memo.get(d, _nil)
if y is not _nil:
return y

cls = type(x)
if issubclass(cls, type):
y = _deepcopy_atomic(x, memo)
else:
copier = _deepcopy_dispatch.get(cls)
if copier is not None:
y = copier(x, memo)
else:
...
reductor = getattr(x, "__deepcopy__", None)
if reductor is not None:
y = reductor(memo)
...
memo[d] = y
...
return y

The memo dict is the cycle-breaker. Before dispatching to any copier the function checks whether id(x) already has an entry and returns the cached copy immediately if so. This handles both cycles (an object that contains itself) and shared references (two fields pointing to the same object), preserving the graph topology of the original.

The sentinel _nil (a module-level list) is used instead of None so that None itself can be stored in memo without ambiguity.

The copier for tuple is special: a tuple containing only atomic elements is returned as-is, but a tuple containing mutable values is rebuilt element-by-element and stored in memo before being returned, because the tuple might be referenced again later in the same graph.

deepcopy for containers (lines 131 to 240)

cpython 3.14 @ ab2d84fe1023/Lib/copy.py#L131-240

def _deepcopy_list(x, memo):
y = []
memo[id(x)] = y # store before recursing
append = y.append
for a in x:
append(deepcopy(a, memo))
return y

def _deepcopy_dict(x, memo):
y = {}
memo[id(x)] = y # store before recursing
for key, value in x.items():
y[deepcopy(key, memo)] = deepcopy(value, memo)
return y

Both _deepcopy_list and _deepcopy_dict store the new empty container in memo before recursing into the elements. This is the critical ordering that makes cycle detection work: if x contains a back-reference to itself, the recursive deepcopy(a, memo) call will find the already-stored y in memo rather than looping forever.

_deepcopy_tuple cannot do the same trick directly because tuples are immutable. Instead it copies all elements first into a list, then constructs the tuple. If the resulting tuple compares equal to the original (all elements were atomic or were the same objects) it returns the original tuple; otherwise it stores and returns the new one.