Python copy: Shallow and Deep Copies

Published on: July 28, 2026
Reading time: 6 minutes
Duplicate document icon representing shallow and deep copies in Python

Assigning a list, dictionary, or class instance to another variable does not automatically create a new object. In Python, assignment usually creates another name that points to the same value. This is why a change made through one variable can unexpectedly appear through another. The Python copy module provides explicit tools for shallow copies, deep copies, and field replacement in immutable-style objects.

This guide explains identity versus equality, copy.copy(), copy.deepcopy(), the internal memo dictionary, custom copying through __copy__() and __deepcopy__(), and the newer copy.replace() API. It complements the guides about Python lists, Python slicing, equality versus identity, Python dictionaries, and Python descriptors.

Assignment is not copying

original = [1, 2, 3]
alias = original

alias.append(4)

print(original)  # [1, 2, 3, 4]
print(alias is original)  # True

Both names point to the same list. The append() call mutates the shared object. The is operator confirms identity: the variables refer to exactly the same object.

This behavior is efficient and usually desirable. A separate copy is needed only when the program requires independent modification.

Shallow copies with copy.copy()

copy.copy() creates a new compound object but reuses references to the objects stored inside it. This is called a shallow copy.

import copy

original = [[1, 2], [3, 4]]
shallow = copy.copy(original)

print(shallow is original)  # False
print(shallow[0] is original[0])  # True

The outer list is new, while the inner lists remain shared. Appending a new row to the copy does not affect the source. Mutating an existing inner row affects both.

shallow.append([5, 6])
shallow[0].append(99)

print(original)  # [[1, 2, 99], [3, 4]]
print(shallow)   # [[1, 2, 99], [3, 4], [5, 6]]

Built-in copying methods

Lists, dictionaries, and sets expose convenient methods:

new_list = old_list.copy()
new_dict = old_dict.copy()
new_set = old_set.copy()

A full slice also creates a shallow list or sequence copy:

new = old[:]

These forms are clear when a function works with a known built-in type. copy.copy() is useful for generic code and custom instances. The official Python copy documentation also notes that some native methods and slicing operations may create a base-type instance when applied to a subclass, while copy.copy() normally preserves the concrete type.

Deep copies with copy.deepcopy()

copy.deepcopy() recursively traverses a compound object and copies its components. This prevents accidental sharing of nested mutable values.

import copy

original = {
    "user": "Ana",
    "permissions": ["read", "edit"],
    "preferences": {"theme": "dark"},
}

deep = copy.deepcopy(original)
deep["permissions"].append("publish")
deep["preferences"]["theme"] = "light"

print(original["permissions"])
print(original["preferences"])

The nested list and dictionary are independent, so modifying the copy does not alter the original.

Deepcopy does not duplicate everything

A deep copy does not guarantee a new identity for every value. Immutable objects, functions, and classes can be reused safely. Modules, open files, sockets, stack frames, windows, and similar runtime resources are not copied generically.

Recursive copying may also copy too much. Connections, locks, shared services, registries, and global caches often should remain shared. Use deepcopy() only when recursive independence is part of the object’s intended contract.

Recursive references

Objects can refer to themselves:

items = []
items.append(items)

A naive recursive copier would enter an infinite loop. deepcopy() prevents this by keeping an internal dictionary named memo. It records objects that have already been copied during the current operation.

import copy

items = []
items.append(items)
cloned = copy.deepcopy(items)

print(cloned is cloned[0])  # True
print(cloned is items)      # False

The self-reference is preserved without reusing the original list.

Why memo matters

The memo dictionary also preserves internal sharing. When two paths in the source graph point to the same object, the copied graph normally points both paths to the same cloned object rather than creating inconsistent duplicates.

import copy

settings = {"limit": 10}
original = [settings, settings]
cloned = copy.deepcopy(original)

print(cloned[0] is cloned[1])  # True
print(cloned[0] is settings)   # False

This behavior is essential for object graphs in which identity relationships carry meaning.

Deep copying can copy too much

Consider a domain object that contains business data and a shared external service:

class Order:
    def __init__(self, items, gateway):
        self.items = items
        self.gateway = gateway

The item data may need an independent copy, while the payment gateway must remain shared. Automatic deep copying may not express this rule correctly. Custom copying solves the problem.

Customizing __copy__()

A class can define __copy__() to control shallow-copy behavior:

import copy

class Document:
    def __init__(self, title, metadata):
        self.title = title
        self.metadata = metadata

    def __copy__(self):
        return type(self)(self.title, self.metadata)

document = Document("Report", {"version": 1})
new_document = copy.copy(document)

print(new_document is document)
print(new_document.metadata is document.metadata)

The document instance is new, while its metadata remains shared, matching shallow semantics.

Customizing __deepcopy__()

__deepcopy__(self, memo) receives the memoization dictionary. Treat it as opaque and pass it to recursive calls of copy.deepcopy().

import copy

class Order:
    def __init__(self, items, gateway):
        self.items = items
        self.gateway = gateway

    def __deepcopy__(self, memo):
        cloned = type(self)(
            copy.deepcopy(self.items, memo),
            self.gateway,
        )
        memo[id(self)] = cloned
        return cloned

The item collection becomes independent, but the service remains shared. Registering the new instance in memo is particularly important for cycles and repeated references.

Immutable updates with copy.replace()

Added in Python 3.13, copy.replace() creates a new object of the same type while replacing selected fields. It is convenient for immutable-style records.

from copy import replace
from dataclasses import dataclass

@dataclass(frozen=True)
class Product:
    name: str
    price: float
    stock: int

original = Product("Keyboard", 120.0, 15)
discounted = replace(original, price=99.9)

print(original)
print(discounted)

The source object remains unchanged. The API supports dataclasses, named tuples, and custom classes implementing __replace__().

copy.replace() is not deepcopy()

replace() replaces named fields and reuses all other values. A mutable field that is not replaced remains shared.

from copy import replace
from dataclasses import dataclass

@dataclass(frozen=True)
class Plan:
    name: str
    steps: list[str]

base = Plan("Launch", ["test", "production"])
new = replace(base, name="Launch v2")

print(new.steps is base.steps)  # True

Use replace() for functional updates to records, not as a synonym for recursive independence.

Implementing __replace__()

Custom classes can participate in the protocol:

class Coordinate:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __replace__(self, **changes):
        allowed = {"x", "y"}
        unknown = set(changes) - allowed
        if unknown:
            raise TypeError(f"Invalid fields: {unknown}")
        return type(self)(
            changes.get("x", self.x),
            changes.get("y", self.y),
        )

After this method is available, copy.replace(coordinate, x=10) creates a new instance with a controlled update.

Nested dictionaries and shallow-copy traps

defaults = {
    "api": {"timeout": 10},
    "logs": {"level": "INFO"},
}

production = defaults.copy()
production["api"]["timeout"] = 30

print(defaults["api"]["timeout"])  # 30

The outer dictionary is copied, but the nested dictionaries are shared. Independent configuration trees require explicit reconstruction, a controlled merge, or a safe deep copy.

Copying lists of objects

Copying a list does not copy the stored instances:

users_copy = users.copy()

print(users_copy is users)       # False
print(users_copy[0] is users[0]) # True

This is useful when the new list represents another ordering or filtered view of the same objects. If every user must be independent, use a domain-specific clone operation or a carefully designed deep copy.

When explicit reconstruction is better

Domain models often benefit from constructing the new object explicitly. The code shows which fields are cloned, recalculated, or intentionally shared.

new_order = Order(
    items=[item.clone() for item in order.items],
    gateway=order.gateway,
)

This approach is more verbose but can communicate ownership more clearly than a generic recursive copy.

Performance and memory

A shallow copy generally costs time proportional to the number of references in the outer container. A deep copy traverses the reachable object graph and may consume substantially more CPU and memory.

Avoid copying large structures repeatedly without measurement. Immutable values, structural sharing, field replacement, generators, and persistent storage may be better alternatives. The official Python data structures tutorial provides useful context about list, dictionary, set, and sequence operations.

Testing the intended independence

Do not test only equality. Verify identity and later mutations:

import copy

original = {"items": [[1], [2]]}
cloned = copy.deepcopy(original)

assert cloned == original
assert cloned is not original
assert cloned["items"] is not original["items"]
assert cloned["items"][0] is not original["items"][0]

cloned["items"][0].append(9)
assert original["items"][0] == [1]

Custom classes should also be tested with cycles, repeated references, shared resources, and subclasses.

Common mistakes

  • Assuming b = a creates a new object.
  • Using list.copy() while expecting nested elements to be duplicated.
  • Applying deepcopy() blindly to sockets, locks, connections, or services.
  • Destroying meaningful identity relationships inside an object graph.
  • Implementing __deepcopy__() without forwarding memo.
  • Treating copy.replace() as a deep copy.
  • Duplicating large structures without measuring cost.
  • Copying when explicit reconstruction would be clearer.

Best practices

  • Define the required level of independence first.
  • Use shallow copies when only the outer container should change.
  • Use deep copies when all reachable mutable state must be independent.
  • Customize classes that own shared runtime resources.
  • Use copy.replace() for functional updates to compatible records.
  • Document which components remain shared.
  • Test equality, identity, and mutations after copying.
  • Benchmark time and memory on large object graphs.

Conclusion

The Python copy module addresses a fundamental distinction: creating another name, copying only the outer object, and recursively duplicating an object graph are different operations. copy.copy() creates a shallow copy. copy.deepcopy() uses memoization to handle cycles and preserve internal sharing. copy.replace() creates updated versions of compatible records by replacing selected fields.

The right choice depends on ownership. Shallow copies are efficient and intentionally share components. Deep copies provide independence but may duplicate too much. Custom protocols combine both strategies when domain objects contain mixed responsibilities. Making this decision explicit prevents surprising mutations, unnecessary memory use, and incorrect duplication of runtime resources.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Document and inbox representing local email stores with Python mailbox
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python mailbox: Local Email Stores

    Learn Python mailbox to read, create, and migrate Maildir, mbox, and MH stores with locking, flags, message parsing, and safe

    Ler mais

    Tempo de leitura: 5 minutos
    12/08/2026
    Text editor representing formatting with Python textwrap
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python textwrap: Format Text

    Learn Python textwrap to wrap, fill, shorten, indent, and dedent text with controlled width, whitespace, long words, and reusable settings.

    Ler mais

    Tempo de leitura: 4 minutos
    10/08/2026
    Folder and magnifying glass representing file-name filters with Python fnmatch
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python fnmatch: Filter File Names

    Learn Python fnmatch to filter file names with shell wildcards, control case sensitivity, exclude patterns, and distinguish glob from regex.

    Ler mais

    Tempo de leitura: 5 minutos
    10/08/2026
    Monitor with binary data representing compact numeric arrays in Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python array: Compact Numeric Data

    Learn Python array for compact numeric storage, binary files, byte order, memory views, and safe buffer interoperability.

    Ler mais

    Tempo de leitura: 6 minutos
    10/08/2026
    Color wheel representing RGB, HSV, and HLS conversions with Python colorsys
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python colorsys: RGB, HSV, and HLS

    Learn Python colorsys to convert colors between RGB, HSV, HLS, and YIQ, generate palettes, and avoid scale and precision mistakes.

    Ler mais

    Tempo de leitura: 5 minutos
    09/08/2026
    Configuration icon representing plist files with Python plistlib
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    plistlib: Read and Write Apple Plist Files

    Learn Python plistlib to read and write XML and binary plist files, validate data, handle dates, bytes, and UIDs safely.

    Ler mais

    Tempo de leitura: 6 minutos
    08/08/2026