Lesson 11: Nested Structures, Aliasing & Copies
Build nested dicts and lists, understand how references behave, and copy safely.
Nested Structures — Data Inside Data
Real-world data is rarely flat. A student dict contains a list of scores. A company is a list of employee dicts. Python handles any nesting depth — you just need to keep the brackets straight:
employees = [
{"name": "Amol", "skills": ["python", "ai"]},
{"name": "Riya", "skills": ["sql", "pandas"]},
]
print(employees[0]["name"]) # Amol
print(employees[0]["skills"][0]) # python
Read nested access outside-in: the outermost bracket first, then work inward.
What You'll Learn in This Lesson
- Build and navigate nested lists and dicts
- Understand aliasing — two names, one object
- Copy safely with shallow vs deep copies
- Distinguish
==(value) fromis(identity)
Aliasing — Two Names, One Object
This is the trap that confuses every beginner:
original = [1, 2, [3, 4]]
alias = original # NOT a copy!
alias.append(99)
print(original) # [1, 2, [3, 4], 99] — original changed too!
When you write b = a, both names point to the SAME list in memory. Change one, and the other changes too. There is only one object — just two labels for it.
Draw arrows:
aliasandoriginalare two arrows pointing at the same box. There is no second box.
Remember Lesson 2's mental model: = moves a name-tag to an object — it never copies the object. Aliasing is that rule biting back: two tags on one object means one change, two visible effects.
Shallow vs Deep Copy
To get an independent copy, you must ask for one — and there are two depths:
| Copy | How | Nested objects? |
|---|---|---|
| Shallow | a.copy(), list(a), a[:] |
Still shared |
| Deep | copy.deepcopy(a) |
Fully independent |
import copy
original = [1, 2, [3, 4]]
shallow = original.copy()
shallow[2].append(100)
print(original) # [1, 2, [3, 4], 100] — inner list STILL shared!
deep = copy.deepcopy(original)
deep[2].append(200)
print(original) # [1, 2, [3, 4], 100] — deep copy is fully independent
- A shallow copy creates a new outer container, but the inner objects are still the same objects.
- A deep copy duplicates everything, recursively — nothing is shared.
Mental model: shallow copy = photocopying the outer box but keeping the same inner boxes inside. Deep copy = rebuilding every single box from scratch.
When is shallow copy safe? When your structure is flat — no nested mutable objects. data = [1, 2, 3] copied shallowly is fully independent, because integers are immutable. The problem only appears with nested lists/dicts.
== vs is vs id
Three different questions:
| Operator | Asks | Example |
|---|---|---|
== |
Do they have the same value? | [1,2] == [1,2] → True |
is |
Are they the same object in memory? | [1,2] is [1,2] → False |
id(x) |
What is the object's memory address? | debugging tool |
a = [1, 2]
b = a # same object
c = [1, 2] # different object, same value
print(a == c) # True — same values
print(a is c) # False — different objects
print(a is b) # True — same object
Use == for comparisons in normal code. Use is for singletons (is None, is True) and identity checks.
Professional rule: always compare to
Nonewithis—if x is None:, neverif x == None:. It is faster, unambiguous, and the universal convention.
The [[0] * 3] * 3 Trap
Creating a matrix the "obvious" way is a classic bug:
bad = [[0] * 3] * 3 # 3 references to the SAME inner list!
bad[0][0] = 1
print(bad) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] — oops!
The * operator repeats references, not copies. The correct way:
good = [[0] * 3 for _ in range(3)] # 3 independent lists
good[0][0] = 1
print(good) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]] — correct!
Why does * on a string or number work fine but fail on a list? Integers and strings are immutable, so sharing is harmless — no one can change a shared 0. Lists are mutable, so every "row" points at the same list you can mutate. The comprehension builds fresh lists each iteration.
Real-World Debugging Flow
When a program "mysteriously" mutates data:
- Ask: did I copy, or did I alias? Search for bare
b = a. - Ask: shallow or deep? Nested structures need
deepcopy. - Verify:
print(id(a), id(b))— identical ids mean one object. - Fix: use
.copy(),[:], orcopy.deepcopy()at the assignment site.
Common Mistakes to Avoid
- Mistake:
b = athen mutatingband seeingachange — Fix: usea[:],.copy(), orcopy.deepcopy(). - Mistake: Shallow copy on nested data and still seeing shared inner lists — Fix: use
copy.deepcopy()for nested structures. - Mistake:
[[0]*3]*3matrices sharing rows — Fix: build with a comprehension. - Mistake:
x == Noneinstead ofx is None— Fix: useisfor singletons. - Mistake: Using
isto compare strings/numbers — Fix:isis for identity; use==for values.
Professional Tips & Tricks
- If you need an independent list, always use
.copy()or slicing — never bare assignment. - Use deepcopy only when structures are nested; it is slower.
- Prefer immutable tuples for data you never change — they cannot be aliased-mutated.
- Compare
Nonewithis, never==. - For nested access, read outside-in and keep the brackets straight.
Key Takeaways
- Nested access reads outside-in; mind the brackets.
- Assignment (
b = a) creates an alias, not a copy. - Shallow copies share inner objects; deep copies share nothing.
==compares values;iscompares identity.- Build matrices with comprehensions, never
[[0]*3]*3. - Use
is None— never== None.
Next up: Module 4 — functions, your reusable building blocks.
import copy
# Nested structure: list of dicts
employees = [
{"name": "Amol", "skills": ["python", "ai"]},
{"name": "Riya", "skills": ["sql", "pandas"]},
]
print("First employee:", employees[0]["name"])
print("Skills:", employees[0]["skills"])
# Aliasing: b refers to the SAME list
original = [1, 2, [3, 4]]
alias = original
alias.append(99)
print("Original mutated by alias:", original)
# Shallow copy: outer is new, inner list still shared
shallow = original.copy()
shallow[2].append(100)
print("Shallow copy affects original inner:", original)
# Deep copy: fully independent
deep = copy.deepcopy(original)
deep[2].append(200)
print("Deep copy leaves original untouched:", original)Lesson Code (Python)
import copy
# Nested structure: list of dicts
employees = [
{"name": "Amol", "skills": ["python", "ai"]},
{"name": "Riya", "skills": ["sql", "pandas"]},
]
print("First employee:", employees[0]["name"])
print("Skills:", employees[0]["skills"])
# Aliasing: b refers to the SAME list
original = [1, 2, [3, 4]]
alias = original
alias.append(99)
print("Original mutated by alias:", original)
# Shallow copy: outer is new, inner list still shared
shallow = original.copy()
shallow[2].append(100)
print("Shallow copy affects original inner:", original)
# Deep copy: fully independent
deep = copy.deepcopy(original)
deep[2].append(200)
print("Deep copy leaves original untouched:", original)Console Output
First employee: Amol
Skills: ['python', 'ai']
Original mutated by alias: [1, 2, [3, 4], 99]
Shallow copy affects original inner: [1, 2, [3, 4], 99, 100]
Deep copy leaves original untouched: [1, 2, [3, 4], 99, 100]Code Visualization Tips
- Draw arrows: alias = second arrow to the same box; copy = a brand-new box.
- Check id(original) vs id(alias) — identical ids mean one object.
- For nested data, picture boxes inside boxes: shallow copy reuses the inner boxes, deep copy rebuilds them all.
Professional Tips & Tricks
- If you need an independent list, always use .copy() or slicing — never bare assignment.
- Use deepcopy only when structures are nested; it is slower.
- prefer immutable tuples for data you never change — they cannot be aliased-mutated.
Python Code Judge & Practice Arena
LeetCode StyleRun real Python 3.12 WebAssembly code directly in your browser against automated test suites.
Problem 1: Deep Copy Isolator 1
Test Your Knowledge
Instant feedbackQuick Check: Lesson 11: Nested Data Structures & Shallow vs Deep Copy
Up next · Continue learning
Functions — Reusable Building Blocks
Define functions, parameters, default arguments, return values, and docstrings.