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 = amakes a second name, not a second object.- Shallow copies duplicate the outer container only.
copy.deepcopyduplicates 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.