10.11 Copying and Mutability
Why b = a shares one list instead of copying it, the list-specific shallow-copy shortcuts (copy() and l[:]), how mutation is visible through every shared reference including function arguments, and the mutable-default-argument trap.
The general mechanics of assignment, shallow copy, and deep copy are covered in depth in 6.5 Copying Objects — this page focuses on what’s specific to lists: the shortcuts, the function-argument consequence, and the classic mutable-default-argument bug.
Assignment Is Not a Copy
b = a makes both names reference the exact same list object — mutating through either name is visible through both.
>>> a = [1, 2, 3]
>>> b = a
>>> b.append(4)
>>> a, a is b
([1, 2, 3, 4], True)
Shallow Copy Shortcuts
l.copy() and l[:] (see 10.3 List Indexing and Slicing) both create a shallow copy — a new outer list, but nested elements are still shared references.
>>> import copy
>>> original = [1, 2, [3, 4]]
>>> shallow = original.copy() # equivalent to original[:] or copy.copy(original)
>>> original[2].append(99)
>>> shallow # sees the change -- the nested list is SHARED
[1, 2, [3, 4, 99]]
Use copy.deepcopy() instead whenever the list contains nested mutable objects and full independence is required — see 6.5 Copying Objects for the complete deep-copy example.
Mutability and Function Arguments
Because a list passed into a function is the same shared object, a function that mutates its parameter mutates the caller’s list too — visible after the call returns.
def modify(lst):
lst.append("modified")
>>> l = [1, 2]
>>> modify(l)
>>> l # caller's list WAS changed
[1, 2, 'modified']
The Mutable Default Argument Trap
Using a list as a function’s default argument value is a classic, list-specific bug: the default is created once, at function-definition time, and shared across every call that doesn’t override it.
>>> def add_item(item, items=[]): # BUG
... items.append(item)
... return items
...
>>> add_item("a")
['a']
>>> add_item("b") # surprise -- 'a' persisted!
['a', 'b']
The fix — the same pattern covered generally in 5.15 Common Mistakes — is to default to None and create a fresh list inside the function body:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Quick Interview Answer
“
b = ashares one list — no copy happens at all.l.copy()andl[:]both make a shallow copy: a new outer list, but any nested mutable objects inside are still shared with the original.copy.deepcopy()is the only one that produces a fully independent structure at every level. This same reference-sharing is exactly why a function that mutates a list parameter changes the caller’s list too, and it’s the root cause of the mutable-default-argument bug: a default list is created once at definition time, not fresh per call, so it silently accumulates state across calls unlessNoneis used as the sentinel instead.”
Common Mistakes
- Assuming
l.copy()produces a fully independent list — it’s shallow; nested mutable objects (like inner lists or dicts) are still shared. - Writing
def f(items=[])and being surprised the default list accumulates values across unrelated calls — useitems=Noneand create the list inside the function instead. - Passing a list into a function and not expecting it to change after the call — if the function shouldn’t mutate the caller’s list, pass a copy explicitly (
f(l.copy())) or have the function copy internally.
Add More Questions to This Guide
Know a question that should be here? Share it and help the community!
Open Google Form