6.4. Aliasing and Copying#
When working with lists, it’s crucial to understand the difference between aliasing, shallow copies, and deep copies. These concepts determine whether changes to one list affect another. The table below summarizes the key differences between aliasing, shallow copy, and deep copy:
Concept |
Outer Object |
Inner Objects |
Syntax |
Changes affect original? |
|---|---|---|---|---|
Aliasing |
Same |
Same |
|
Yes |
Shallow Copy |
New |
Same |
|
Sometimes (when nested) |
Deep Copy |
New |
New |
|
No |
import sys
from pathlib import Path
# Find project root by looking for _config.yml
current = Path.cwd()
for parent in [current, *current.parents]:
if (parent / '_config.yml').exists():
project_root = parent
break
else:
project_root = Path.cwd().parent.parent
# Add project root to path
sys.path.insert(0, str(project_root))
# Import shared teaching helpers and cell magics
from shared import thinkpython, diagram, jupyturtle, structshape
from shared.download import download
6.4.1. Aliasing#
Aliasing occurs when two or more variables point to the same object in memory. When you do a variable assignment using = in Python, you’re not copying the object—you’re creating another variable that points to the same object. This second variable is an alias.
Both variables refer to the same list object, so any modification through either variable affects the same underlying list:
# Aliasing: both variables point to the same list
original = [1, 2, 3, 4, 5]
alias = original ### NOT a copy!
alias[0] = 999 ### Modify through the alias
print("Original:\t", original) # check to see if original is modified
print("Alias:\t\t", alias)
print("ID of original:\t", id(original)) ### check the memory address of original
print("ID of alias:\t", id(alias)) ### check the memory address of alias
print("Same object?\t", original is alias) # True
Original: [999, 2, 3, 4, 5]
Alias: [999, 2, 3, 4, 5]
ID of original: 4493515328
ID of alias: 4493515328
Same object? True
Contrast with immutable strings:
# Contrast with immutable strings
from os import name
name1 = "Chen"
print(name1, id(name1)) # "Chen"
name2 = name1 # Alias created
print(name2, id(name2)) # "Chen" - same object
name2 = "Alice" # Creates NEW object, reassigns name2
print(name2, id(name2)) # "Alice" - NEW object
print(name1, id(name1)) # "Chen" - UNCHANGED!
print("Same ID?", id(name1) == id(name2)) # Check if name1 and name2 point to the same object (should be False)
Chen 4493938192
Chen 4493938192
Alice 4493939248
Chen 4493938192
Same ID? False
Aliasing is useful for efficiency (no copying needed), but can cause unexpected behavior if you modify mutable objects, thinking you have independent copies.
6.4.2. Shallow Copy#
To create a shallow copy, you can use
list slicing
[:],the
.copy()method,the
list()function, orcopy.copy().
import copy
a = [1, 2, 3]
b = a[:] # list slicing
b = a.copy() # copy() method
b = list(a) # list() function
b = copy.copy(a) # copy module
All these methods create a new list object, but if the list contains other mutable objects (like nested lists), those nested objects are not copied—only their references are copied.
A shallow copy creates a new object while retaining references to the objects contained in the original. It only copies the top-level structure without duplicating nested elements.
For simple 1-D lists (containing only immutable objects like numbers, strings, or booleans), shallow copying works perfectly fine and you shall see the new lists all have different ID’s.
# four ways to create a shallow copy
import copy
letters = ['a', 'b', 'c', 'd']
letters_copy1 = letters[:] # list slicing
letters_copy2 = letters.copy() # the copy() method
letters_copy3 = list(letters) # the list function
letters_copy4 = copy.copy(letters) # using the copy module
print(letters_copy1, id(letters_copy1))
print(letters_copy2, id(letters_copy2))
print(letters_copy3, id(letters_copy3))
print(letters_copy4, id(letters_copy4))
print("All copies have the same contents?", letters_copy1 == letters_copy2 == letters_copy3 == letters_copy4) # True, contents are the same
print("letters_copy1 is letters?", letters_copy1 is letters) # False, different objects in memory
print("letters_copy2 is letters?", letters_copy2 is letters) # False, different objects in memory
print("letters_copy3 is letters?", letters_copy3 is letters) # False, different objects in memory
print("letters_copy4 is letters?", letters_copy4 is letters) # False, different objects in memory
['a', 'b', 'c', 'd'] 4490503808
['a', 'b', 'c', 'd'] 4490506496
['a', 'b', 'c', 'd'] 4494006848
['a', 'b', 'c', 'd'] 4490505408
All copies have the same contents? True
letters_copy1 is letters? False
letters_copy2 is letters? False
letters_copy3 is letters? False
letters_copy4 is letters? False
### simpler example: shallow copy works fine for 1-D lists: Update
original = [1, 2, 3, 4, 5]
shallow = original[:] # Creates a new list
print("Same value?\t", original == shallow) # True
print("Same object?\t", original is shallow) # False
# Modify the shallow copy
shallow[4] = 999
print("update shallow:\t", shallow)
print("Original:\t", original) # Original is unchanged
print("Shallow:\t", shallow) # Only the copy is modified
Same value? True
Same object? False
update shallow: [1, 2, 3, 4, 999]
Original: [1, 2, 3, 4, 5]
Shallow: [1, 2, 3, 4, 999]
However, for nested lists (lists containing other lists), shallow copy shares references to the nested objects:
# Using copy.copy() with nested lists
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
print("original-shallow same value?\t", original == shallow) # True - contents are the same
print("original-shallow same object?\t", id(original) == id(shallow)) # False - different list objects
shallow[0].append(99)
print("update shallow (99):\t\t", shallow)
print("is original updated?\t\t", original) # [[1, 2, 99], [3, 4]] - nested list affected!
print("same nested object?\t\t", shallow[0] is original[0]) # True - same nested list object
original-shallow same value? True
original-shallow same object? False
update shallow (99): [[1, 2, 99], [3, 4]]
is original updated? [[1, 2, 99], [3, 4]]
same nested object? True
Now that shallow copy has given us two variable names referencing the same object, which we prefer not to happen in most cases. Same thing happens with using slicing for shallow copy with nested lists.
# Using slicing with nested lists
original = [[1, 2, 3], [4, 5, 6]]
shallow = original[:] # or original.copy()
# Modify the nested list
shallow[0][0] = 999
print("Original:", original) # Original is also modified!
print("Shallow:", shallow)
print("Same nested object?", shallow[0] is original[0]) # True - same nested list object
Original: [[999, 2, 3], [4, 5, 6]]
Shallow: [[999, 2, 3], [4, 5, 6]]
Same nested object? True
As you can see, modifying the nested list in shallow also affects original because they share references to the same inner lists.
6.4.3. Deep Copy#
To create a deep copy that copies all nested objects recursively, use the copy module’s deepcopy() function. This creates completely independent copies of all nested structures. Deep copy creates a new object and recursively copies all nested objects—everything is independent.
import copy
original = [[1, 2, 3], [4, 5, 6]]
deep = copy.deepcopy(original)
print("Original:", original) # Original is unchanged
print("Deep copy:", deep) # Only the deep copy is modified
print("original-deep same value?", original == deep) # True - contents are the same
print("original-deep same object?", id(original) == id(deep)) # False - different list objects
print("same nested object?", deep[0] is original[0]) # False - different nested list objects
# Modify the nested list
print
deep[0][0] = 999
print("Original:", original) # Original is unchanged
print("Deep copy:", deep) # Only the deep copy is modified
Original: [[1, 2, 3], [4, 5, 6]]
Deep copy: [[1, 2, 3], [4, 5, 6]]
original-deep same value? True
original-deep same object? False
same nested object? False
Original: [[1, 2, 3], [4, 5, 6]]
Deep copy: [[999, 2, 3], [4, 5, 6]]
### simpler example with deep copy
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
print(id(original))
print(id(deep))
deep[0].append(99)
print(original) # [[1, 2], [3, 4]] - original unchanged!
print(deep) # [[1, 2, 99], [3, 4]] - only deep copy changed
print(deep[0] is original[0]) # False - different nested list objects
4491474624
4466402240
[[1, 2], [3, 4]]
[[1, 2, 99], [3, 4]]
False