References, copies & the mutable-default trap
Names share objects: build new lists instead of mutating, and never use a mutable default argument.
A name is a label, not a box
You pass lists and dicts between functions all day. If you believe assignment copies them, you get the worst class of bug: a value mutates somewhere you never touched, and the broken read is nowhere near the accidental write. Knowing exactly what shares an object is what separates code that scales from code that corrupts state under you.
The model: names bind to objects
Every value in Python is an object living somewhere in memory. A variable is just a name bound to that object, not a box holding a copy. Assignment binds a second name to the same object:
a = [1, 2, 3]
b = a # b binds to the SAME list, no copy happens
b.append(4)
print(a) # [1, 2, 3, 4]
print(a is b) # True, one list with two names
a is b asks "same object?" (identity), while a == b asks "same value?" (equality). The demo below shows this exactly: mutating through b is visible through a because there is only one list.
▸ a = [1, 2, 3]b = ab.append(4)c = a[:]c.append(99)
- aL1
- L1list[1, 2, 3]
one list, named a
Build new instead of mutating
When a function should return a changed version, build a fresh list and leave the input alone. This is what the Apply exercise wants:
def doubled(nums):
return [n * 2 for n in nums] # new list; nums is untouched
The comprehension allocates a new list, so the caller's data is safe. Prefer this over looping and calling nums.append(...), which would edit the caller's list in place.
For each line, does the new name end up on the same object, or is a second object created?
Copy on purpose: shallow vs deep
When you genuinely need a separate copy, do it deliberately. A slice a[:] or list(a) makes a shallow copy: a new outer list holding the same inner objects.
c = a[:] # new outer list
c.append(99)
print(a is c) # False, independent outer lists
For nested structures, a shallow copy still shares the inner objects, so editing grid[0][0] through the copy changes the original. Use copy.deepcopy when you need full independence:
import copy
deep = copy.deepcopy(grid) # inner lists copied too
The mutable-default trap
A default value is evaluated once, when the def statement runs, not on each call. So a mutable default is one shared object reused across every call:
def bad(item, bucket=[]): # the SAME list every call
bucket.append(item)
return bucket
bad("a") # ["a"]
bad("b") # ["a", "b"], the previous call leaked in
def append_new(value, bucket=None): # the safe pattern
if bucket is None:
bucket = [] # fresh list each call
bucket.append(value)
return bucket
append_new is the Practice exercise: use None as the sentinel and create the list inside.
Interview nuance: default arguments are evaluated exactly once at function-definition time and stored on the function object (you can inspect bad.__defaults__, a tuple holding that one shared list). That is why bucket=[] accumulates across calls and bucket=None plus an inside-the-body [] does not. Interviewers use this to check whether you understand when Python evaluates expressions, not just what the syntax looks like.
Step through both versions and watch the one shared default list accumulate, then the None pattern build a fresh list per call:
▸ def bad(item, bucket=[]):bad('a')bad('b')append_new('a', bucket=None) # bucket = [] inside
- bad.__defaults__[0]D1
- D1list[]
The default list is created ONCE, when def runs, and stored on the function object.
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4], same list!
c = a[:] # a real (shallow) copy
c.append(99)
print(a) # unchanged by cApply
Your turn
The task this lesson builds to.
Implement doubled(nums): return a new list where every number is doubled, without changing
the original nums.
For [1, 2, 3] return [2, 4, 6].
2 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Implement append_new(value, bucket=None): append value to bucket and return it, but when no
bucket is given, start a fresh list (avoid the mutable-default trap).
append_new(1, [2, 3]) returns [2, 3, 1]; append_new("a") returns ["a"].
3 hints and 3 automated checks are waiting in the workspace.