🐍 Python Course · Stage 3 · Lesson 54 of 89 · copy — Copy Objects
Course Overview →

🔵 Read Beginner and Intermediate tabs. The Expert tab has the internals — worth reading once you finish Stage 3.

← shutil — File Operations concurrent.futures — Parallelism →
thecodex.expert · The Codex Family of Knowledge
Python Standard Library

copy — Shallow and Deep Copy Operations

The copy module duplicates objects — but the difference between a shallow copy and a deep copy is the source of some of Python’s most common and confusing bugs.

import copy docs.python.org Last verified:
Canonical Definition

The copy module provides copy() for shallow copies and deepcopy() for deep copies of arbitrary Python objects. A shallow copy duplicates the outer object but shares references to nested objects; a deep copy recursively duplicates everything, producing a fully independent object.

🟩 Beginner

Real duplicates, not just new names

What you will learn: why b = a does not copy, the difference between assignment and copying, and how to make a shallow copy.

How to read this tab: The = is not a copy lesson is foundational — internalise it before moving to shallow vs deep.

⏱ 25 min📄 2 sections🔶 Prerequisite: Object-Oriented Python
The idea

Assigning b = a does not copy a list — it makes b another name for the same list, so changing one changes the other. The copy module makes real duplicates. But there are two kinds — shallow and deep — and knowing which you need prevents a whole class of baffling bugs.

= is not a copy. b = a creates a new name for the same object. For mutable objects (lists, dicts, custom classes), changes through one name show through the other. This never matters for immutable values, but for mutable ones you must copy explicitly to get independence.

Why copying is tricky

Pythonthe_problem.py
# Assignment does NOT copy — both names point to the SAME object
original = [1, 2, 3]
alias = original           # NOT a copy — same list
alias.append(4)
print(original)            # [1, 2, 3, 4] — changed too! They're the same list.

# Check identity: they ARE the same object
print(original is alias)   # True

# To get a real copy, use the copy module (or list-specific methods)
import copy
real_copy = copy.copy(original)   # a separate list
real_copy.append(99)
print(original)            # [1, 2, 3, 4] — unaffected this time
print(original is real_copy)  # False — different objects

# Lists also have shortcut copy methods (all shallow):
shortcut1 = original[:]        # slice copy
shortcut2 = original.copy()    # .copy() method
shortcut3 = list(original)     # constructor
= is not a copy

b = a creates a new name for the same object, not a new object. This is fine for immutable values (numbers, strings) where it never matters, but for mutable objects (lists, dicts, sets, custom classes) it means changes through one name are visible through the other. When you need independence, you must copy explicitly.

A shallow copy shares the nested objects. copy.copy duplicates the outer container but the inner objects remain shared — modifying a nested list shows in both. This is the shallow-copy trap: one level of copying is not enough when your object contains other mutable objects.

Shallow copy — one level deep

Pythonshallow.py
import copy

# A shallow copy duplicates the OUTER object but shares the INNER ones
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)

# The outer list is independent — appending to one doesn't affect the other
shallow.append([5, 6])
print(original)        # [[1, 2], [3, 4]] — outer unaffected

# BUT the inner lists are SHARED — they're the same objects
shallow[0].append(99)
print(original)        # [[1, 2, 99], [3, 4]] — inner list changed!
print(original[0] is shallow[0])   # True — same inner list

# This is the shallow-copy trap: one level of copying isn't enough
# when your object contains other mutable objects.

✅ Beginner tab complete

  • I know b = a creates a new name for the same object, not a copy
  • I can make a copy with copy.copy() or list/dict .copy()
  • I know copying only matters for mutable objects
  • I understand identity (is) vs equality (==)

Continue to pickle →

🔵 Intermediate

Shallow vs deep copy

What you will learn: how a shallow copy shares nested objects while a deep copy duplicates everything, and how to customise copying with __copy__/__deepcopy__.

How to read this tab: This is the heart of the page: shallow shares the contents, deep duplicates them. Run the nested examples.

⏱ 25 min📄 2 sections🔶 Prerequisite: Beginner tab
💡

deepcopy duplicates everything, but costs time. It recursively copies every nested object, giving full independence — and correctly handles cycles. But it is slower and uses more memory, so use it only when you genuinely need deep independence. For flat structures, a shallow copy is faster and sufficient.

Deep copy — all the way down

A deep copy recursively duplicates everything, so the copy shares nothing with the original. This is what you usually want for nested structures you intend to modify independently.

Pythondeep.py
import copy

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

# Now the inner lists are independent too
deep[0].append(99)
print(original)        # [[1, 2], [3, 4]] — completely unaffected
print(original[0] is deep[0])   # False — different inner lists

# Real-world example: copying a nested configuration
config = {
    "server": {"host": "localhost", "ports": [8000, 8001]},
    "debug": True,
}

# Shallow copy shares the nested dict and list — dangerous
shallow = copy.copy(config)
shallow["server"]["host"] = "production"
print(config["server"]["host"])   # 'production' — original changed!

# Deep copy is safe for independent modification
config2 = copy.deepcopy(config)
config2["server"]["host"] = "staging"
print(config["server"]["host"])   # 'production' — original safe

# Deep copy handles cycles correctly (objects referencing themselves)
a = [1, 2]
a.append(a)            # a contains itself!
b = copy.deepcopy(a)   # works — deepcopy tracks already-copied objects
print(b[2] is b)       # True — the cycle is preserved in the copy
Deep copy can be slow

deepcopy recursively duplicates every nested object, which costs time and memory for large structures. Use it when you genuinely need full independence. For flat structures (no nested mutable objects), a shallow copy is faster and sufficient. Do not reach for deepcopy reflexively.

💡

Pass memo through __deepcopy__. When customising deep copy, thread the memo dict through nested deepcopy calls — failing to pass it breaks cycle detection and shared-structure preservation for everything beneath that object.

Controlling how objects are copied

Pythoncustom.py
import copy

# Classes can control copying via __copy__ and __deepcopy__
class Document:
    def __init__(self, title, tags):
        self.title = title
        self.tags = tags
        self.cache = {}          # expensive, shouldn't be copied

    def __copy__(self):
        # Custom shallow copy — share tags, fresh cache
        new = Document(self.title, self.tags)
        return new

    def __deepcopy__(self, memo):
        # memo prevents infinite loops on cyclic references
        new = Document(
            copy.deepcopy(self.title, memo),
            copy.deepcopy(self.tags, memo),
        )
        return new               # cache deliberately left empty

doc = Document("Report", ["draft", "2026"])
clone = copy.deepcopy(doc)
print(clone.title, clone.tags)   # Report ['draft', '2026']

# The 'memo' dict maps id(original) -> copy, so shared/cyclic
# references are copied once and reused — preserving structure.
Commonly confused
Shallow vs deep — the one-sentence rule. A shallow copy duplicates the container but shares the contents; a deep copy duplicates everything. If your object contains other mutable objects you intend to modify, you need deepcopy. If it is flat, shallow is enough and faster.
dict.copy() and list.copy() are shallow. The built-in .copy() methods, slicing, and constructors all make shallow copies. For nested structures, only copy.deepcopy() gives full independence.

✅ Intermediate tab complete

  • I know shallow copy shares nested objects; deep copy duplicates them
  • I use deepcopy when nested mutable objects must be independent
  • I know deepcopy handles cyclic references correctly
  • I can customise copying with __copy__ and __deepcopy__

Continue to pickle →

🔴 Expert

The memo dict and the pickle connection

What you will learn: how the memo dictionary enables cycle handling and shared-structure preservation, and why copy falls back to the pickle protocol.

How to read this tab: Read to understand exactly how deepcopy preserves shared references and why some objects cannot be copied.

⏱ 20 min📄 1 section🔶 Prerequisite: After Stage 2

The memo dictionary, __reduce__, and what gets shared

The engine of deepcopy is the memo dictionary — a map from id() of each original object to its copy. Before copying any object, deepcopy checks the memo; if the object has already been copied, it reuses that copy rather than making a second one. This single mechanism accomplishes two critical things: it handles cyclic references without infinite recursion, and it preserves shared structure, so that if two parts of your object graph point to the same sub-object, the copy's two parts also point to a single shared copy rather than two separate duplicates.

Pythoncopy_internals.py
import copy

# Shared structure is preserved by the memo
shared = [1, 2, 3]
original = {"a": shared, "b": shared}   # both keys point to ONE list
print(original["a"] is original["b"])   # True

deep = copy.deepcopy(original)
print(deep["a"] is deep["b"])           # True — STILL shared in the copy!
# deepcopy copied 'shared' once and reused it for both keys.

# What deepcopy does NOT copy (treated as atomic / immutable):
# int, float, str, bool, None, and tuples of immutables are not
# duplicated — they're immutable, so sharing is safe.
x = "immutable string"
print(copy.deepcopy(x) is x)            # True — no copy needed

# How copy decides: it dispatches on type via copy._deepcopy_dispatch,
# falls back to __deepcopy__, then __reduce_ex__ (the pickle protocol).
# This is why most objects are deep-copyable without any custom code —
# the same machinery that powers pickle powers deepcopy.

# Objects that CANNOT be meaningfully copied (files, sockets, locks)
# should define __copy__/__deepcopy__ or __reduce__ to handle it,
# or raise. By default, copying them may share the underlying resource.

# Manual memo use — pre-seed to control copying
memo = {}
original = [1, 2, 3]
memo[id(original)] = original   # tell deepcopy: reuse, don't copy this
result = copy.deepcopy({"data": original}, memo)
print(result["data"] is original)   # True — we forced sharing

The fallback chain reveals why copy and pickle are deeply related: when an object defines no __copy__ or __deepcopy__, the copy module dispatches to __reduce_ex__ — the same protocol pickle uses to serialize objects. This is why the vast majority of objects are deep-copyable with no special code, and also why objects that cannot be pickled (open files, sockets, locks, generators) generally cannot be deep-copied either, and require an explicit __deepcopy__ that decides how to handle the unpicklable resource. For controlling this, defining __deepcopy__(self, memo) and threading the memo through any nested deepcopy calls is essential — failing to pass memo breaks cycle detection and shared-structure preservation for everything beneath that object.

✅ Expert tab complete

  • I know the memo dict maps id(original) to copy, handling cycles
  • I know deepcopy preserves shared structure (one shared object stays shared)
  • I know copy falls back to __reduce_ex__ (the pickle protocol)
  • I know unpicklable objects (files, sockets) generally cannot be deep-copied

Continue to pickle →

Sources

1
Python Standard Library — copy. docs.python.org/3/library/copy.html.
2
Python Standard Library — copy.deepcopy and the memo dictionary. docs.python.org/3/library/copy.html.
3
Python Standard Library — __reduce_ex__ (shared copy/pickle protocol). docs.python.org/3/library/pickle.html.
Source confidence: High Last verified: Primary source: docs.python.org copy