Blog

Shallow Copy vs Deep Copy in Python

Assigning a list to a second name does not copy anything. Both names point at the same object, so a change through one is visible through the other.

Copying has two levels: a shallow copy makes a new outer object holding the same inner objects, and a deep copy rebuilds the whole tree.

Assignment is not a copy

One list, two names.

# assignment does not copy
original = [1, 2, 3]
alias = original
alias.append(4)
print(original, alias is original)

It prints:

[1, 2, 3, 4] True

A shallow copy

.copy(), list(x) and x[:] all do the same thing for a list.

# a shallow copy is a new outer object
shallow = original.copy()          # list(original) and original[:] do the same
shallow.append(5)
print(original, shallow, shallow is original)

It prints:

[1, 2, 3, 4] [1, 2, 3, 4, 5] False

The outer list is now separate.

Where shallow stops

The new list holds the same inner lists as the old one.

# shallow means one level
rows = [[1, 2], [3, 4]]
copied = rows.copy()
copied[0].append(99)
print(rows, copied)
print('inner list shared:', rows[0] is copied[0])

It prints:

[[1, 2, 99], [3, 4]] [[1, 2, 99], [3, 4]]
inner list shared: True

Both outer lists show the change, because there is only one inner list.

Deep copy

copy.deepcopy walks the structure and copies every object it finds.

# deepcopy copies the whole tree
import copy

rows2 = [[1, 2], [3, 4]]
deep = copy.deepcopy(rows2)
deep[0].append(99)
print(rows2, deep)
print('inner list shared:', rows2[0] is deep[0])

It prints:

[[1, 2], [3, 4]] [[1, 2, 99], [3, 4]]
inner list shared: False

The change is contained.

Dicts and nested config

This is the usual real world case: a config dict that some other code mutates.

# it works on dicts and objects too
config = {'db': {'host': 'localhost', 'ports': [5432]}}
shallow_cfg = copy.copy(config)
deep_cfg = copy.deepcopy(config)
config['db']['ports'].append(5433)
print('shallow sees the change:', shallow_cfg['db']['ports'])
print('deep does not:', deep_cfg['db']['ports'])

It prints:

shallow sees the change: [5432, 5433]
deep does not: [5432]

Cycles are handled

deepcopy keeps a memo of what it has already copied, so a structure that points at itself does not cause infinite recursion.

# cycles are handled
node = {'name': 'root'}
node['self'] = node
clone = copy.deepcopy(node)
print(clone['self'] is clone, clone is node)

It prints:

True False

The copy points at the copy.

A tuple does not protect what is inside it

A tuple cannot be reassigned, but the lists inside it can still be changed, and a shallow copy shares them.

# tuples of mutables are still shared by a shallow copy
data = ([1, 2], [3, 4])
shallow_t = copy.copy(data)
shallow_t[0].append(3)
print(data, shallow_t[0] is data[0])

It prints:

([1, 2, 3], [3, 4]) True

Immutable containers hold mutable contents. That is worth remembering when you use a tuple as a default value.

What to remember

  • b = a makes a second name, not a second object.
  • Shallow copies duplicate the outer container only.
  • copy.deepcopy duplicates the whole tree and handles cycles.
  • Deep copying is slow. Use it when you need it, not by default.

If you are deep copying often, it is usually a sign the data should be immutable instead. A frozen dataclass or a tuple of tuples removes the question.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Leave a Reply