SECTION 1
Historical Context & Motivation
Since the earliest days of programming, developers have wrestled with a subtle but powerful idea: when two variable names point to the same piece of data in memory, changing one silently changes the other. This behavior is called aliasing, and it has been responsible for some of the most confusing bugs in software history. Understanding aliasing is essential because Python, the language you are learning, relies heavily on reference-based variable assignment for lists, dictionaries, and other mutable objects.
The concept of aliasing dates back to the very foundations of computer science. Early languages like FORTRAN allowed multiple names for the same memory location, and programmers quickly discovered that "shared state" caused hard-to-trace errors. As languages evolved, the problem did not disappear—it simply took new forms. Python, created in 1991, made many things simpler for beginners, but its object model means that aliasing bugs are still a common pitfall.
1957 • FORTRAN Introduces Shared Memory
1972 • C Pointers Make Aliasing Explicit
1991 • Python Hides Pointers Behind References
2001 • The copy Module Is Standardized
copy module, giving developers reliable tools to avoid aliasing.2020s • Modern Linters Warn About Aliasing
The central question this lesson addresses is straightforward: when you write b = a in Python and a is a list (or a list of lists), does b get its own independent copy, or are a and b just two names for the exact same object? The answer has enormous consequences for your code's correctness.
SECTION 2
Core Principles & Definitions
Before you can fix aliasing bugs, you need to understand the vocabulary and mental model behind them. Python treats every piece of data—numbers, strings, lists, dictionaries—as an object stored somewhere in your computer's memory. A variable name is simply a label that references (points to) that object. Multiple labels can reference the same object, and that is where aliasing begins.
Aliasing
Shallow Copy
Deep Copy
Mutable vs. Immutable
Identity vs. Equality
is checks if two names point to the same object (identity). == checks if two objects have the same value (equality). Aliases satisfy both.KEY TAKEAWAY
b = a does not duplicate the box—it just sticks a second label on the same box. If the box contains smaller boxes (a nested list), a shallow copy gives you a new outer box but the smaller boxes inside are still the originals. A deep copy builds you an entirely new set of boxes.SECTION 3
Visual Explanation — How Aliasing Works in Memory
The diagram below shows what happens in memory when you create a nested list and then assign it to another variable. Pay close attention to the arrows: they represent references (pointers) to objects in memory. When two arrows point to the same box, you have an alias.
b = a, both variables point to the same outer list, which in turn points to the same inner lists. Bottom half: mutating b[0][0] changes the shared inner list, so a[0][0] is also affected.Notice in the top half of the diagram that both a and b have arrows going to the exact same outer list object. That object, in turn, stores references to two inner lists. Because no copying happened, every level of the structure is shared. The bottom half shows the consequence: a single mutation through b is visible through a as well. This is the classic aliasing bug.
SECTION 4
How Copying Works Under the Hood
Python gives you three main strategies for duplicating data structures, each with a different level of independence from the original. Understanding the difference is the key to squashing aliasing bugs.
Strategy 1: Simple Assignment (No Copy)
b = a creates a new name that references the same object. No data is duplicated. Use a is b to confirm they share identity—it will return True. This is never safe when you intend to modify the copy without affecting the original.
Strategy 2: Shallow Copy
A shallow copy creates a brand-new outer container and fills it with references to the same inner objects. You can create one with b = a.copy(), b = list(a), or b = a[:]. After a shallow copy, a is b returns False (different outer objects), but a[0] is b[0] returns True if the elements are mutable objects like lists. This means shallow copies are safe for flat (non-nested) structures but dangerous for nested ones.
Strategy 3: Deep Copy
A deep copy recursively duplicates every object at every level of nesting. You use import copy and then b = copy.deepcopy(a). After this, a is b is False and a[0] is b[0] is also False. Modifying b at any depth has zero effect on a. This is the safest option for nested structures.
| Operation | New Outer? | New Inner? | Safe for Nested? |
|---|---|---|---|
b = a | No | No | ❌ No |
b = a.copy() | Yes | No | ❌ No |
b = copy.deepcopy(a) | Yes | Yes | ✅ Yes |
Comparison of three copy strategies for lists
SECTION 5
Shallow Copy vs. Deep Copy — A Side-by-Side View
The diagram below compares a shallow copy and a deep copy of the same nested list. Focus on where the arrows point: in the shallow copy, the inner lists are still shared, while in the deep copy, every object is independent.
b[0][0] mutates a as well. Right: a deep copy creates independent inner lists, so a is unaffected.On the left side of the diagram, both the a outer list and the b outer list point to the same inner list objects (shown by the dashed arrows converging on the same boxes). Any mutation to those inner lists is visible through either name. On the right side, copy.deepcopy has created entirely separate inner list objects (shown in green), so changes to b's inner lists cannot reach a's inner lists.
When Is Shallow Copy Enough?
SECTION 6
Worked Example — Fixing an Aliasing Bug in a Grid
Suppose you are writing a tic-tac-toe game. You want to create a 3×3 grid initialized with empty strings. A common beginner approach accidentally creates aliased rows. Let's walk through the bug, diagnose it, and fix it.
Debugging an Aliased Grid
- 1
Step 1 — Write the Buggy Code
A student writes the following code to create a 3×3 grid:row = ["", "", ""]grid = [row, row, row]This creates a list containing three references to the same row list. - 2
Step 2 — Observe the Bug
The student places an 'X' in the top-left cell:grid[0][0] = "X"Expected result: only row 0 has an 'X'. Actual result:print(grid)→[["X","",""],["X","",""],["X","",""]]All three rows show 'X' because they are all aliases of the same list object.Bug confirmed:grid[0] is grid[1]returnsTrue - 3
Step 3 — Diagnose with id()
Python's built-inid()function returns the memory address of an object. Runningprint(id(grid[0]), id(grid[1]), id(grid[2]))shows the same number three times, confirming that all three entries point to the same object in memory. - 4
Step 4 — Fix with Independent Lists
Replace the buggy code with a list comprehension that creates a new list on each iteration:grid = [["", "", ""] for _ in range(3)]Now each row is a distinct object.grid[0] is grid[1]returnsFalse.Fixed! Settinggrid[0][0] = "X"now only affects row 0. - 5
Step 5 — Verify the Fix
grid[0][0] = "X"print(grid)→[["X","",""],["","",""],["","",""]]Only the first row was modified, exactly as intended.Output:[["X","",""],["","",""],["","",""]]
The Multiplication Trap
[[""] * 3] * 3 looks like a quick shortcut, but it produces the exact same aliasing bug. The * 3 on the outer level copies the reference, not the list. Always use a list comprehension for grids.SECTION 7
Strengths & Limitations of Each Approach
Each copying strategy has trade-offs in terms of safety, performance, and readability. The table below summarizes when to use each one so you can make a smart choice in your own code.
| Strategy | Strengths | Limitations |
|---|---|---|
Assignment (b = a) | Instant, no extra memory, perfect when you intentionally want two names for the same data. | Any mutation through one name affects the other. Not a copy at all. |
Shallow Copy (.copy()) | Fast and memory-efficient. Perfectly safe for flat lists of immutable items. | Inner mutable objects are still shared. Gives a false sense of security with nested data. |
Deep Copy (copy.deepcopy()) | Fully independent clone. Safe at every nesting level. The most defensive choice. | Slower and uses more memory. Can cause issues with objects that contain circular references. |
| List Comprehension (manual) | Explicit and readable. Creates new objects for each iteration. No import needed. | Only copies one level of nesting per comprehension. More code to write for deeply nested structures. |
Comparison of copy strategies for Python data structures
KEY TAKEAWAY
SECTION 8
Connection to Advanced Concepts
The aliasing ideas you have learned here appear in many advanced areas of computer science. Understanding references and copying is foundational to topics like object-oriented programming, where objects frequently reference each other, and functional programming, which avoids aliasing by treating data as immutable.
| What You Know Now | Where It Leads |
|---|---|
| Variables are references to objects | In Java/C++, you'll distinguish between "pass by reference" and "pass by value" when calling functions. |
copy.deepcopy() for nested structures | In data science, libraries like NumPy and Pandas have their own copy behaviors (.copy(deep=True)) that you must understand to avoid corrupting datasets. |
| Mutable default arguments cause bugs | This is a famous Python gotcha: def f(x=[]): shares the same list across all calls. You'll learn to use None as a default instead. |
Using is to check identity | In multi-threaded programs, shared mutable state (aliasing across threads) is the #1 source of concurrency bugs. |
How aliasing concepts connect to advanced topics
As you continue your programming journey, you will encounter aliasing in increasingly subtle forms. Some languages, like Rust, were specifically designed to prevent aliasing bugs at compile time through a system called ownership and borrowing. For now, the skills you have built here—checking identity with is, choosing the right copy method, and using list comprehensions for grids—will serve you well in Python and beyond.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
b = a does not create a copy of a list a. What does it create instead?Show answer
b = a does not duplicate the list stored in memory. Instead, it creates a second name (or label) that references the exact same list object. Both a and b point to one list, so any change made through b (like appending an element or changing a value) is immediately visible through a. You can verify this by checking a is b, which will return True, confirming they share identity.Question 2: BASIC CALCULATION
x = [10, 20, 30]
y = x.copy()
y[0] = 99
print(x[0])Show answer
10. Since x is a flat list containing only immutable integers, a shallow copy via .copy() is sufficient. The variable y becomes a completely new list object. Changing y[0] to 99 replaces the reference inside y without affecting x. Integers are immutable, so shallow copy is safe here.Question 3: INTERMEDIATE
a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(5)
b.append([7, 8])
print(a)
print(len(a), len(b))Show answer
[[1, 2, 5], [3, 4]] and the second print outputs 2 3. Here's why: b = a.copy() creates a new outer list but the inner lists are shared. So b[0].append(5) modifies the inner list that a[0] also references, making a[0] become [1, 2, 5]. However, b.append([7, 8]) modifies the outer list of b only, so a still has length 2 while b has length 3.Question 4: APPLIED
board (an 8×8 list of lists). Explain why a simple board.copy() would fail.Show answer
import copy and then snapshot = copy.deepcopy(board). A simple board.copy() would create a new outer list but each row inside would still be the same list object as in board. When the player makes a move and you modify board[row][col], the snapshot's row would also change because it references the same inner list. Alternatively, you could write snapshot = [row[:] for row in board] which creates a new copy of each row using slicing. Both approaches ensure that the snapshot is independent of the live board.Question 5: CRITICAL THINKING
def add_row(grid, row):
grid.append(row)
return grid
A student calls it like this:
r = [0, 0, 0]
g = []
for i in range(3):
add_row(g, r)
g[0][0] = 5
print(g)
Predict the output. Then rewrite the loop to avoid the aliasing bug, explaining two different valid approaches.Show answer
[[5, 0, 0], [5, 0, 0], [5, 0, 0]]. The same list object r is appended to g three times, so all three rows are aliases. Changing g[0][0] affects all rows. Approach 1: inside the loop, pass a fresh copy each time: add_row(g, r.copy()). This creates a new list for each row. Approach 2: rewrite the function so it copies internally: change grid.append(row) to grid.append(row[:]). Both approaches ensure each row in the grid is an independent list object.SUMMARY
Lesson Summary
Summary
In Python, variables are references to objects, not containers for values. When you write b = a on a mutable object like a list, you create an alias—two names pointing to the same data. Changes through one name are visible through the other, which is the root cause of aliasing bugs. For flat lists of immutable items, a shallow copy (via .copy() or slicing) is sufficient. For nested structures (lists of lists, dicts of lists), you must use copy.deepcopy() or a list comprehension that explicitly builds new inner objects.
To debug aliasing, use the is operator or id() to check whether two variables reference the same object. Watch out for the multiplication trap ([[]] * n) and always prefer list comprehensions for creating grids. These skills will carry forward into object-oriented programming, data science, and any language that uses reference semantics.