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

    Binary search and sorted lists with Python bisect
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python bisect: Search Sorted Lists

    Learn Python bisect for binary search, ordered insertion, duplicate ranges, thresholds, and safe sorted-list design.

    Ler mais

    Tempo de leitura: 4 minutos
    08/08/2026
    Code and packaged files with Python importlib.resources
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python importlib.resources: Practical Guide

    Learn Python importlib.resources to access packaged files safely across wheels, installed applications, and custom import loaders.

    Ler mais

    Tempo de leitura: 5 minutos
    07/08/2026
    Keyboard and data flow representing multiple-file processing with Python fileinput
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python fileinput: Read Multiple Files

    Learn Python fileinput to read multiple files or stdin, track line numbers, open compressed logs, and rewrite content safely with

    Ler mais

    Tempo de leitura: 7 minutos
    07/08/2026
    Code editor with numbered lines representing the Python linecache module
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python linecache: Read Lines by Number

    Learn Python linecache to read source lines by number, manage cached files, refresh changed code, and support traceback and import

    Ler mais

    Tempo de leitura: 7 minutos
    07/08/2026
    Binary data representing internal serialization with Python marshal
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python marshal: Internal Serialization

    Learn Python marshal to serialize internal types, control format versions, and reject code objects when they are unnecessary.

    Ler mais

    Tempo de leitura: 6 minutos
    06/08/2026
    Monitor with binary code representing pickle customization with Python copyreg
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python copyreg: Customize Pickle

    Learn Python copyreg to register reduction functions, customize pickle, and preserve object compatibility across versions.

    Ler mais

    Tempo de leitura: 5 minutos
    06/08/2026