Why Changing One Changed the Other
You copied the list, changed the copy, and the original changed with it. Point three different kinds of copy at the same nested list, mutate one item at the bottom, and see precisely which of them flinch.
Worth reading first: Names Are Not Boxes
You copied the list, changed the copy, and the original changed with it. Point three different kinds of copy at the same nested list, mutate one item at the bottom, and see precisely which of them flinch.
A copy that copied only the outside
The chapter on names established that assignment never copies anything — it points a second name at the same object. You already know to reach for .copy() when you want a separate list. Here is where that stops being enough.
>>> a = [[1, 2], [3, 4]]>>> b = a.copy()>>> b[0].append(9)>>> a[[1, 2, 9], [3, 4]]b is a genuinely separate outer list. It is also holding the exact same two inner lists that a holds, because copying a list copies what its slots contain — and what its slots contain are references. One level was duplicated. Everything below it was shared.
Three ways to copy a flat list, all of which work
When a list holds only numbers or strings, the distinction never surfaces, and all three of these are equivalent and correct.
original = [1, 2, 3] safe = original.copy()safe = list(original)safe = original[:]A number cannot be mutated in place, so sharing one is harmless — there is no operation that would change it for both names. That is why flat lists let you get away with the shallow copy indefinitely, and why the problem only appears once a list starts holding things that can change.
The same three ways on a nested list
All three still copy exactly one level. Try the two buttons below under each mode: the first mutates an inner list, the second mutates the outer one, and it takes both to tell all three cases apart.
a [[1, 2], [3, 4]]
b [[1, 2], [3, 4]]
b is a → True b[0] is a[0] → True
Press both buttons under each mode. The first tells deepcopy apart from the other two; the second is the only one that tells b = a apart from b = a.copy().
Notice what the second button proves. Mutating the inner list looks identical for b = a and b = a.copy(), so an inner mutation alone can never tell you which of the two you have. Only appending to the outer list separates them.
deepcopy follows every level down
When you need a copy that shares nothing at all, the standard library has one. It is not a built-in, and it does not need to be.
from copy import deepcopy a = [[1, 2], [3, 4]]b = deepcopy(a)b[0].append(9) print(a) # [[1, 2], [3, 4]]print(b) # [[1, 2, 9], [3, 4]]deepcopy walks the whole structure and rebuilds every mutable thing it finds, however deep. It also tracks what it has already copied, so a structure that refers to itself does not send it into an infinite loop — which is more care than the problem usually gets credit for.
What deepcopy costs you
It is not free, and reaching for it by default is its own mistake.
Shallow is enough when
The list holds only numbers, strings, or tuples of them — anything that cannot be changed in place. Sharing something unchangeable costs nothing and saves the walk.
Go deep when
The structure holds lists, dictionaries, or objects that something is going to mutate, and the two copies must be able to diverge. That is the only case that justifies the cost.
The cost is real: deepcopy visits every object in the structure and allocates a new one for each. On a large nested structure copied inside a loop, that is usually the slowest line in the program — and often it was protecting against a mutation that never happens.
The mutable default argument, seen again
You met this trap when functions were introduced. It belongs here too, because it is the same mechanism wearing different clothes: one object, shared by everyone who reaches it.
def add_badge(badge, badges=[]): badges.append(badge) return badges print(add_badge("solder")) # ['solder']print(add_badge("cad")) # ['solder', 'cad']The default list was created once, when the function was defined, and every call that does not supply its own has been appending to that same list ever since. The fix is the standard one — default to None and build a fresh list inside the body.
def add_badge(badge, badges=None): if badges is None: badges = [] badges.append(badge) return badgesKey takeaways
- .copy(), list(x), and x[:] all copy exactly one level. On a flat list that is a complete copy; on a nested one it is not.
- A shallow copy of a nested list gives you a new outer list holding the very same inner lists — mutating one of those is visible through both names.
- Mutating an inner list cannot tell b = a apart from b = a.copy(). Only changing the outer list reveals which one you have.
- deepcopy rebuilds every mutable object at every level, and handles structures that refer to themselves without looping forever.
- deepcopy costs a full walk and a new object per item. Use it when the copies must diverge, not as a reflex.
- Sharing something that cannot be mutated — a number, a string, a tuple of them — is always safe, which is why flat lists never show the problem.
- A mutable default argument is the same trap: one object created at definition time, shared by every call that does not override it.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.