Skip to main content

copy.py

Implements copy() for shallow copies and deepcopy() for full recursive copies. The module is self-contained: no C extension backing it. All dispatch is done through dunder methods and internal lookup tables.

Map​

LinesSymbolRole
1–40module header, _copy_dispatchAtomic-type fast path table
41–90copy()Shallow copy entry point
91–200deepcopy()Recursive copy with memo
201–240_deepcopy_dispatchType-keyed deep-copy handlers
241–270_reconstruct()Rebuild via __reduce_ex__
271–300Error, helpersException and registration API

Reading​

_copy_dispatch and the shallow path​

copy() first checks cls.__copy__. Failing that it looks up the type in _copy_dispatch, a plain dict that maps atomic types to the identity function.

# CPython: Lib/copy.py:44 copy
def copy(x):
cls = type(x)
copier = _copy_dispatch.get(cls)
if copier:
return copier(x)
copier = getattr(cls, "__copy__", None)
if copier is not None:
return copier()
...

int, float, str, bytes, bool, and type all map to the identity function because they are immutable. No allocation needed.

deepcopy() and the memo dict​

deepcopy() accepts an optional memo dict. The dict maps id(original) to the already-copied object. Checking the memo before doing any work breaks circular references and also deduplicates shared subobjects.

# CPython: Lib/copy.py:128 deepcopy
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)
copier = _deepcopy_dispatch.get(cls)
if copier is not None:
y = copier(x, memo)
...
memo[d] = y
return y

The sentinel _nil is a module-level list, chosen because no user object can equal it.

__deepcopy__ protocol​

If the type defines __deepcopy__, deepcopy() calls it with the memo dict and short-circuits all other dispatch. This lets containers like numpy.ndarray implement their own deep-copy semantics.

# CPython: Lib/copy.py:148 deepcopy
copier = getattr(cls, "__deepcopy__", None)
if copier is not None:
y = copier(memo)

_reconstruct() fallback​

When no dedicated handler exists, deepcopy() calls __reduce_ex__(4) and passes the result to _reconstruct(). This reuses the same protocol used by pickle, so any picklable object is also deep-copyable.

# CPython: Lib/copy.py:241 _reconstruct
def _reconstruct(x, memo, func, args, state=None, listiter=None, dictiter=None, *, deepcopy=deepcopy):
y = func(*args)
memo[id(x)] = y
if state is not None:
...
y.__setstate__(deepstate)

gopy notes​

  • _copy_dispatch is a plain map[reflect.Type]func(Object) Object in gopy.
  • The memo dict becomes map[uintptr]Object keyed by pointer identity.
  • __copy__ / __deepcopy__ are looked up via the normal objects.LookupAttr path; no special casing required.
  • _reconstruct depends on __reduce_ex__, which requires the pickle protocol stubs tracked under task #481.

CPython 3.14 changes​

  • 3.13 added an optimised fast path for dataclasses using __replace__; 3.14 keeps it and extends it to subclasses of dataclasses.
  • The _deepcopy_atomic helper was inlined for several built-in types to cut one function-call level from the hot path.