Aliasing and Copying

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

b = a

Yes

Shallow Copy

New

Same

[:], copy(), list()

Sometimes (when nested)

Deep Copy

New

New

copy.deepcopy()

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, or

  • copy.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