Mutability, Shallow Copy & Deep Copy
IntermediateAssignment shares, copy() duplicates one level, deepcopy() duplicates everything — knowing which you need prevents the most common "my data changed by itself" bugs.
Overview
Python never copies on assignment — it shares references. That is fine for immutable objects (int, str, tuple) because they cannot change under you, but for lists/dicts/sets and objects, three levels exist: assignment (same object), shallow copy (new container, SAME inner objects), and deep copy (recursively new everything). Function arguments follow the same rule — mutable arguments can be modified by the callee, a behaviour interviewers probe with "is Python pass-by-value or pass-by-reference?" (answer: pass-by-object-reference).
Three Levels: Assign vs copy() vs deepcopy()
A shallow copy is a new outer container whose slots point to the same inner objects — mutating a NESTED item is visible through both. deepcopy severs everything.
import copy
teams = [["asha", "ravi"], ["neha"]]
alias = teams # level 0: same object
shallow = teams.copy() # level 1: new list, same inner lists
deep = copy.deepcopy(teams) # level 2: everything new
teams[1].append("kiran") # mutate a NESTED list
print(alias[1]) # ['neha', 'kiran'] — same object, obviously
print(shallow[1]) # ['neha', 'kiran'] — SHALLOW shares inner lists!
print(deep[1]) # ['neha'] — deep copy unaffected
# Shallow copy spellings (equivalent):
a = teams.copy(); b = teams[:]; c = list(teams)
# For flat lists of immutables, shallow is all you need
nums = [1, 2, 3]
safe = nums.copy()
safe.append(4) # nums untouchedMutable Arguments — Pass by Object Reference
The callee receives the same object. Mutating it (append) affects the caller; REBINDING the parameter (=) does not. Functions that mutate inputs should say so — or copy first.
def add_bonus(scores): # receives the SAME list object
scores.append(100) # caller sees this!
def replace(scores):
scores = [0, 0] # rebinds LOCAL name only — caller unaffected
marks = [80, 90]
add_bonus(marks)
print(marks) # [80, 90, 100] — mutated!
replace(marks)
print(marks) # [80, 90, 100] — rebinding didn't escape
# Defensive pattern — don't surprise your caller
def normalized(scores):
result = scores.copy() # work on a copy
result.sort()
return result
# int/str/tuple arguments are safe — immutable objects can't changeKey Points to Remember
- 1Assignment NEVER copies — it binds another name to the same object
- 2Shallow copy (copy(), [:], list()) duplicates one level; deepcopy() recurses
- 3Python is pass-by-object-reference: callees can mutate, not rebind, your objects
- 4Immutables (int, str, tuple, frozenset) are immune to all of this — a reason to prefer them
Interview Questions
Sign in to ask AriaIs Python pass-by-value or pass-by-reference? Demonstrate with a list.
Shallow vs deep copy — construct an example where shallow copy is a bug.
Why is a mutable default argument dangerous, given what you know about references?
Ask Aria about Mutability, Shallow Copy & Deep Copy
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.