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

    Monitor showing binary search and sorted Python lists
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python bisect: Keep Lists Sorted

    Learn Python bisect to keep lists sorted, locate ranges, find neighbors, and insert values efficiently with binary search.

    Ler mais

    Tempo de leitura: 7 minutos
    27/07/2026
    Developer implementing a priority queue with Python heapq
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python heapq: Build Priority Queues

    Learn Python heapq to build priority queues, find the smallest values, and process tasks efficiently with binary heaps.

    Ler mais

    Tempo de leitura: 6 minutos
    26/07/2026
    Logo do Python com expressão pensativa sobreposto a uma biblioteca com estantes cheias de livros, representando o conceito de bibliotecas em Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python Libraries: What They Are and How to Use Them

    Learn what Python libraries are, how modules and packages differ, how to install and import them, use virtual environments, and

    Ler mais

    Tempo de leitura: 6 minutos
    10/07/2026
    Estatísticas e análise de dados com Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python Built-in Functions: Complete Guide

    Learn the most useful Python built-in functions for input, output, conversion, numbers, collections, iteration, sorting, validation, files, inspection, and objects.

    Ler mais

    Tempo de leitura: 4 minutos
    10/07/2026
    Logo do Python com o texto 'requests' abaixo
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python Requests: Complete Beginner Guide

    Learn Python Requests from scratch: install the library, send GET and POST requests, work with parameters, headers, JSON, timeouts, sessions,

    Ler mais

    Tempo de leitura: 4 minutos
    10/07/2026
    Manipulação de datas e calendário em Python
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python datetime: Work with Dates and Times

    Learn Python datetime: create, parse, format, compare, and calculate dates and times, use timedelta, UTC, zoneinfo, and aware datetimes.

    Ler mais

    Tempo de leitura: 5 minutos
    10/07/2026