Lesson 05
Aliasing, Copying, and Mutation Traps
Master one of Python's most common interview and real-world bugs: understanding when two variables share the same mutable object and when they don't.
Learning roadmap
| Topic | Key idea |
|---|---|
| Aliasing | b = a → same object |
| Shallow copy | New container |
| Mutation trap | Shared mutable objects |
is vs == |
Identity vs value |
From Item Assignment and id(), you know rebinding vs in-place edits. This lesson covers what happens when two names share one mutable object.
Visual overview
There are only three relationships you need to remember.
Memory rule: Alias → 1 object · Copy → 2 objects · Immutable → new object after change.
1. Aliasing — two names, one object
An alias means multiple variables point to the same memory object.
Step 1: Create one list
lst1 = [10, 20, 30]Step 2: Create an alias
lst2 = lst1Both variables reference one list.
Step 3: Mutate through lst2
lst2.append(40)
print(lst1)
print(lst2)Output:
[10, 20, 30, 40]
[10, 20, 30, 40]Memory formula
2. Shallow copy — new container
A shallow copy creates a new outer object.
lst1 = [10, 20, 30]
lst3 = lst1.copy()
lst3.append(99)
print(lst1)
print(lst3)
print(lst1 is lst3)Output:
[10, 20, 30]
[10, 20, 30, 99]
False
.copy()gives the list its own memory address. Works similarly fordict.copy()andset.copy().
3. The nested list mutation trap
This is the most common interview question.
Question: Why does changing the copy still affect the original?
Step 1: Nested structure
Step 2: Shallow copy
The outer list is copied. The inner lists are shared.
a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(99)
print(a)
print(b)Output:
[[1, 2, 99], [3, 4]]
[[1, 2, 99], [3, 4]]Why? Only the outer list was copied. The inner lists still have the same identity.
For nested structures, use
copy.deepcopy().
import copy
b = copy.deepcopy(a)4. Mutable default argument trap
Python creates default arguments once, not on every function call.
The bug
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item("A"))
print(add_item("B"))Output:
['A']
['A', 'B']Why this happens
Correct solution
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucketNow every call gets a fresh list.
5. is vs ==
This is another classic interview topic.
a = [1, 2]
b = [1, 2]
print(a == b)
print(a is b)Output:
True
False| Operator | Compares |
|---|---|
== |
Values |
is |
Memory identity |
Related: is / is not
When should you copy?
| Situation | Best practice |
|---|---|
| Mutating a list in a function | Use list.copy() |
| Duplicating a dictionary | dict.copy() |
| Nested lists/dicts | copy.deepcopy() |
| Compare values | == |
| Check same object | is |
Playground
Try this complete example.
lst1 = [10, 20, 30]
lst2 = lst1
lst2.append(40)
print("lst1:", lst1)
print("same object?", lst1 is lst2)
lst3 = lst1.copy()
lst3.append(99)
print("after copy append lst1:", lst1)
print("lst1 is lst3?", lst1 is lst3)Observe:
lst2changeslst1lst3is independentischecks identity
Use the lesson playground at the bottom of the page to run this code live.
Quick knowledge check
-
After
b = aon a list, doesb.append(1)affecta?
Answer: Yes — both variables are aliases of the same object. -
After
b = a.copy(), does mutation affecta?
Answer: No — the outer container is a different object. -
Why are mutable default arguments dangerous?
Answer: Python creates the default list once, then reuses it across function calls.
30-second interview revision
| Concept | Remember |
|---|---|
b = a |
Alias (same object) |
b = a.copy() |
New outer object |
| Nested copy | Inner objects still shared |
== |
Compare values |
is |
Compare identity |
Golden rule: If two variables point to the same mutable object, changing one changes the other. Use .copy() for a new container, and deepcopy() for nested structures.
Next: Practice Programs.
Loading playground…