A shallow copy makes a new outer object that shares references to the same inner objects as the original. A deep copy makes a new outer object AND recursively copies every inner object, producing total independence. For flat data (numbers, strings, tuples of immutables) the difference is invisible. For nested mutable data (lists of lists, dicts of dicts, custom objects holding references) the difference is the source of countless production bugs. Python ships the standard copy module with copy.copy() and copy.deepcopy(); plus four equivalent shallow-copy idioms most tutorials skip, and a brand-new copy.replace() in Python 3.13. This piece is one of 17 short explainers in our Python Concepts Explained reference.

Key Takeaways

  • Assignment is not a copy. b = a makes b a second name for the same object. Mutate through one name, see the change through the other.
  • Four idiomatic shallow copies of a list: copy.copy(a), a.copy(), a[:], list(a). For dicts: copy.copy(d), d.copy(), {**d}, dict(d). All produce a new outer container; inner items are still shared.
  • Deep copy goes all the way down. copy.deepcopy(a) recursively copies everything. Safe but slow; expect 10-100× slower than shallow on nested structures.
  • The tuple-of-mutables trap. A tuple is immutable, but (a, b) = ([1], [2]) contains mutable inner lists. A shallow copy gives you a new tuple object pointing to the SAME inner lists. Surprising and frequently exploited as an interview question.
  • Recursive objects are safe with deepcopy. The memo dict tracks objects already copied during the operation, so a = []; a.append(a) deepcopies into a self-referencing structure instead of infinite-looping.
Memory layout: original, shallow copy, deep copy Memory layout for shallow vs deep copy Original: a = [1, [2, 3], 4] a (original) [0] 1 [1] [2] 4 b = a.copy() (shallow) [0] 1 [1] [2] 4 c = deepcopy(a) (deep) [0] 1 [1] [2] 4 shared inner list (a and b BOTH point here) 2 3 independent copy (c's own inner list) 2 3 The consequence: b[1].append(99) mutates the shared inner list, so a[1] == [2, 3, 99] too. c[1].append(99) mutates only c's independent inner list. a stays unchanged. Shallow is one layer of independence. Deep is independence all the way down.
Three outer lists, two inner lists. The shallow copy shares the inner list with the original; the deep copy gets its own.

Assignment Is Not a Copy

The most common confusion isn't about shallow versus deep; it's about thinking = copies at all. It doesn't.
a = [1, 2, 3]
b = a            # b is a SECOND NAME for the same list
b.append(4)
print(a)         # [1, 2, 3, 4]   ← a "changed" too
The single object on the right has two names. Mutate through one name, see the change through the other. There is no copy involved anywhere.

When does this matter?

For immutable values (numbers, strings, tuples of immutables, frozensets) the distinction is invisible. You can't mutate an integer, so two names pointing at one integer is the same as two separate integers. For mutable containers (lists, dicts, sets, most custom objects) the distinction is everything. If you intend an independent copy, you have to ask for one explicitly: the rest of this article is about how.

The slicing connection

The "copy via slicing" idiom b = a[:] comes from the same machinery covered in our slicing explainer. A full-range slice constructs a new list containing the same references; the rule that "slicing returns a new list" is what makes it a shallow copy. The shallow-copy semantics are therefore inherited from slicing, not an extra feature bolted on; once you internalize the slicing model, the copy semantics fall out for free. The same applies to list(a) and {**d}: any operation that constructs a new container from an existing one produces a shallow copy by default.Abstract data visualization with arrows and nodes, evoking memory layout of copied objects

Shallow Copy: Four Idiomatic Ways to Make One

Most tutorials show copy.copy() and stop. In practice, Python ships at least four equally valid shallow-copy idioms per container type. They all produce identical results; pick by readability.

For lists

a = [1, [2, 3], 4]

# 1. The copy module (works on any object)
import copy
b1 = copy.copy(a)

# 2. The list method
b2 = a.copy()

# 3. Slicing the whole list
b3 = a[:]

# 4. The list constructor
b4 = list(a)
All four give a new outer list. The inner [2, 3] is shared in every case. a.copy() is the most explicit; a[:] is the oldest idiom and still common; list(a) reads cleanest when you want to also convert from another iterable type.

For dicts

d = {"a": 1, "b": [2, 3]}

# 1. The copy module
e1 = copy.copy(d)

# 2. The dict method
e2 = d.copy()

# 3. Spread into a new literal (Python 3.5+)
e3 = {**d}

# 4. The dict constructor
e4 = dict(d)
Same story: new outer dict, inner list [2, 3] shared. For the dict-merge variations and the | operator from Python 3.9, see our dict-merge explainer.

For sets and other containers

s.copy(), copy.copy(s), and set(s) all work. For custom classes, copy.copy(obj) is the universal answer; the object can define __copy__ to customize the behavior, covered below.

Deep Copy: When the Tree Goes All the Way Down

If shallow only duplicates the outer container, deep duplicates everything reachable. copy.deepcopy() walks the entire object graph and creates a parallel structure with no shared mutable references.
import copy

a = [1, [2, 3], 4]
c = copy.deepcopy(a)

c[1].append(99)
print(a)   # [1, [2, 3], 4]      ← unchanged
print(c)   # [1, [2, 3, 99], 4]
The cost is real: deepcopy visits every object in the graph, allocates a new copy, and tracks already-seen objects to handle cycles. For deeply nested structures this can be 100× slower than a shallow copy. Use it deliberately, not by reflex.

The Tuple-of-Mutables Trap

The interview question that catches mid-level Python developers: tuples are immutable, so a shallow copy of a tuple is "the same thing", right? Almost.
t = ([1, 2], [3, 4])
t2 = copy.copy(t)

# Different tuple objects?
print(t is t2)     # depends on implementation; sometimes True
# Same inner lists?
print(t[0] is t2[0])   # True   ← SHARED

t2[0].append(99)
print(t)    # ([1, 2, 99], [3, 4])   ← original tuple "mutated"!
The tuple itself didn't change (it still references the same two list objects), but the list it references did. CPython actually short-circuits copy.copy(t) on tuples and returns the same tuple, since immutable objects don't need duplicating. The inner lists are mutable, though, and they're shared either way.
Rule: the immutability of a container does not imply the immutability of its contents. frozenset of frozen elements is fully immutable; tuple of lists is not. Reach for deepcopy whenever the inner objects can be mutated and you need independence.

Recursive Objects and the memo Dict

Naively, deep-copying a self-referencing structure should infinite-loop. a = []; a.append(a) contains itself; following the references recursively would never terminate. deepcopy handles this through a memoization dictionary that maps each original object's id() to its copy.
a = [1, 2]
a.append(a)
print(a)   # [1, 2, [...]]   ← Python detects the cycle and prints ...

b = copy.deepcopy(a)
print(b)        # [1, 2, [...]]
print(b[2] is b)   # True   ← new outer list self-references CORRECTLY
print(b is a)      # False   ← but it's not the original
The memo dict is the second positional argument to deepcopy. You almost never pass it manually; the recursion supplies it. The mechanic also handles structures where the same object appears at multiple positions, copying it once and reusing the copy at every reference. That's why deepcopy on a graph of N nodes runs in O(N), not O(N²).

Custom Classes: __copy__ and __deepcopy__

Default behavior for a custom class: copy.copy creates a new instance and shallow-copies __dict__; copy.deepcopy does the same but recurses into every attribute. For most plain classes that's fine. When it isn't, override:
import copy

class Connection:
    def __init__(self, host, cache=None):
        self.host = host
        self.cache = cache or {}
        self.socket = open_socket(host)   # expensive resource

    def __copy__(self):
        # Skip the socket; reuse the existing one
        new = Connection.__new__(Connection)
        new.host  = self.host
        new.cache = self.cache.copy()
        new.socket = self.socket
        return new

    def __deepcopy__(self, memo):
        # Full duplicate, fresh socket
        new = Connection.__new__(Connection)
        new.host  = self.host
        new.cache = copy.deepcopy(self.cache, memo)
        new.socket = open_socket(self.host)
        return new
The __deepcopy__ protocol receives the memo dict so the override can pass it along when recursing into attributes. The most common reason to define these methods: avoid duplicating expensive resources (sockets, file handles, database connections) and avoid sharing things you really need fresh (random number generator state, caches).

copy.replace(): Python 3.13's Newcomer

Python 3.13 added copy.replace(obj, **changes) to the standard library. It creates a new object identical to obj with specified fields replaced. Previously, dataclasses.replace() did this for dataclasses only; the new copy.replace() generalizes to any class implementing __replace__.
from dataclasses import dataclass
import copy

@dataclass
class Point:
    x: int
    y: int

p = Point(1, 2)
q = copy.replace(p, y=99)
print(q)        # Point(x=1, y=99)
print(p)        # Point(x=1, y=2)   ← original untouched
For dataclasses, namedtuples, and frozen records, copy.replace() is cleaner than copy.deepcopy(p) plus mutation (and works on frozen objects where mutation would fail). Reach for it whenever you want "the same record but with one field different" (Python docs).

Performance: deepcopy Is Slow

Rough timings for copying a nested list of 1,000 sublists, each with 100 integers (CPython 3.12):
MethodTimeNotes
a[:] / list(a) / a.copy()~50 µsbaseline shallow
copy.copy(a)~80 µsshallow with module overhead
copy.deepcopy(a)~5,000 µs~100× slower than shallow
pickle.loads(pickle.dumps(a))~3,500 µssometimes faster than deepcopy on pure data
The pickle round-trip is a common production trick when deepcopy is too slow: it skips Python's recursion machinery in favor of optimized C serialization. It only works on picklable objects (no open files, no lambdas, no socket connections), but for plain data structures it can win meaningfully. For domain objects where the deepcopy traversal is the bottleneck, also consider whether the structure really needs a deep copy at all.

JSON Round-Trip: the Poor Man's Deepcopy

One more workaround appears in real codebases: serialize to JSON and parse back.
import json

a = {"users": [{"name": "alice", "tags": ["admin", "x"]}, {"name": "bob", "tags": ["y"]}]}
b = json.loads(json.dumps(a))

b["users"][0]["tags"].append("removed")
print(a["users"][0]["tags"])   # ['admin', 'x']   ← original untouched
The JSON round-trip is the third common "force a deep copy" trick after copy.deepcopy and pickle.loads(pickle.dumps(...)). It works only on JSON-compatible data (dicts, lists, strings, numbers, bools, None). It silently converts tuples to lists, drops dict keys that aren't strings, and rejects sets, dates, and custom objects outright. The upside: it's the fastest of the three on pure data, and the output is portable to any language that speaks JSON.

When to reach for each

  • copy.deepcopy: default choice. Handles any Python object including custom classes and cycles. Slowest of the three.
  • pickle.loads(pickle.dumps(...)): faster than deepcopy on pure data. Handles most Python objects (no open files, no lambdas). Output is Python-only.
  • json.loads(json.dumps(...)): fastest, but works only on JSON-typed data and loses tuples, sets, and non-string keys. Output is portable.
For domain code, deepcopy is the safe default; only reach for serialization round-trips when you've actually measured the bottleneck.

Decision Table

SituationUse this
Need a second name for the same objectb = a (no copy)
Flat list/dict of immutables, need new containershallow: a.copy() or list(a)
Nested list/dict of mutables, mutate both sidesdeep: copy.deepcopy(a)
Custom class with expensive attributesdefine __copy__ / __deepcopy__
Dataclass / namedtuple with one field differentcopy.replace(obj, field=new) (3.13+)
Deepcopy too slow on pure datapickle.loads(pickle.dumps(a))

Common Mistakes

Five traps to watch for:Mistake 1: assuming = copies. The single most common Python bug. b = a is a second name; mutations are visible through both. Use any shallow-copy idiom when you want a new container.Mistake 2: assuming a[:] is deep. It's shallow. So is a.copy(), list(a), copy.copy(a), {**d}, and every other one-liner. Deep needs the explicit copy.deepcopy().Mistake 3: reaching for deepcopy on flat data. copy.deepcopy([1, 2, 3]) works, but it's 50× slower than list(a) for the same result. Use shallow when the data is flat.Mistake 4: forgetting that tuples can hide mutables. copy.copy((a, b)) returns the same tuple (since tuples are immutable). The inner mutable objects are shared. If you intended independence, deepcopy.Mistake 5: implementing only __copy__ and forgetting __deepcopy__. The two are separate protocols. A class with __copy__ but no __deepcopy__ uses default deep-copy semantics, which may not match the intent of the shallow override. Define both whenever you define either.

Frequently Asked Questions

What is the difference between shallow copy and deep copy in Python?

A shallow copy makes a new outer object but reuses references to the inner objects. A deep copy makes a new outer object AND recursively copies every inner object. For flat data (numbers, strings, tuples of immutables), the difference doesn't matter because the inner items can't be mutated. For nested mutable data (lists of lists, dicts of dicts), a shallow copy still shares inner objects, so mutating one side can change the other; a deep copy gives full independence.

Does a[:] make a deep copy in Python?

No. a[:] makes a shallow copy. So do a.copy(), list(a), [*a], copy.copy(a), and several other idioms. They all create a new outer list with references to the same inner objects. For independence at every level, you need copy.deepcopy(a) from the copy module.

When should I use deepcopy in Python?

Use deepcopy when the data contains nested mutable structures (lists of lists, dicts of dicts, mixed) AND both sides will be mutated independently. Avoid deepcopy when the data is flat or holds only immutables (numbers, strings, tuples of immutables); a shallow copy is faster and produces identical results. Also avoid deepcopy on huge structures unless you really need it: it can be 100 times slower than shallow copy.

Can you deepcopy a recursive object in Python?

Yes. copy.deepcopy maintains a memo dictionary that tracks objects already copied during the operation. When the recursion meets an object whose id() is already in the memo, it reuses the previously-made copy instead of looping forever. So a = [1, 2]; a.append(a) can be deepcopied safely; the resulting structure self-references the new outer list, not the original.

What is copy.replace in Python 3.13?

copy.replace(obj, **changes) creates a new copy of obj with the specified attributes replaced. It works on dataclasses, namedtuples, and any class implementing __replace__. It's the standard-library generalization of dataclasses.replace(), introduced in Python 3.13. For immutable record-like data, copy.replace() is cleaner than building a new object by hand or reaching for deepcopy plus mutation.

The Bottom Line: One Layer or All the Way Down

Shallow copy duplicates the outer container; deep copy duplicates every reachable object. For flat immutables, the choice is invisible. For nested mutables, the choice is the difference between two clean snapshots and one tangled shared structure. Memorize the four shallow-copy idioms, reach for copy.deepcopy only when you actually need it, and remember that the immutability of a container says nothing about the immutability of its contents. With those three facts, every "why did my list change?" bug becomes a 30-second diagnosis. For the rest of the most-asked Python concept questions, browse the full Python Concepts Explained index.

Drill the Copy Patterns Until They're Reflex

CodeGym's Python track turns the shallow-vs-deep distinction into muscle memory through 800+ hands-on tasks across 62 levels. The AI validator checks every submission in seconds; the AI mentor explains what broke when you get stuck. First level free; full plan on the pricing page. Learn Python on the free track →

Learn more about our mission and terms of service. Published article was last reviewed on 2026-06-02.