Skill 10: Aliasing & Mutation (Python)
1. Variable Assignment vs Object Modification
What to Teach
Python, like most languages, distinguishes between changing what a variable points to (assignment) versus changing the contents of the object the variable points to (mutation). It is, however, syntactically much more subtle than it is in Pyret.
Assignment (Changes Variable, Not Object)
# assignment creates new objects or points to different objects
x = [1, 2, 3]
x = [4, 5, 6] # x now points to a different list object
name = "Alice"
name = "Bob" # name now points to a different string object
Mutation (Changes Object Contents)
# mutation changes the contents of existing objects
numbers = [1, 2, 3]
numbers[0] = 10 # modifies the existing list object
numbers.append(4) # modifies the existing list object
# numbers is still the same list object, but its contents changed
# Dataclass field modification
from dataclasses import dataclass
@dataclass
class Student:
name: str
gpa: float
student = Student("Alice", 3.5)
student.name = "Alicia" # modifies field of existing object
student.gpa = 3.8 # modifies field of existing object
Teaching Examples
Assignment Example:
def reassign_variable():
"""Demonstrates variable reassignment."""
grades = [85, 90, 78]
print(f"Original grades: {grades}")
# this is assignment as it creates new list
grades = [95, 88, 92]
print(f"After assignment: {grades}")
# the original [85, 90, 78] list still exists but grades no longer points to it
def modify_list_contents():
"""demonstrates list mutation."""
grades = [85, 90, 78]
print(f"Original grades: {grades}")
# this is mutation - changes existing list
grades[0] = 95 # change first element
grades.append(88) # add new element
print(f"After mutation: {grades}")
# same list object, but contents changed
What to Point Out:
- Mutation changes the contents of the existing object
- Assignment creates/points to new objects
- Mutation modifies existing objects in place
Common Student Mistakes to Watch For
-
Confusing assignment with mutation
# students might think these do the same thing
my_list = [1, 2, 3]
my_list = my_list + [4] # assignment - new list
my_list = [1, 2, 3]
my_list.append(4) # mutation - same list, new contents -
Expecting assignment to modify original objects
def broken_update(data: list[int]) -> None:
data = [1, 2, 3] # assignment - doesn't change original list
original = [4, 5, 6]
broken_update(original)
print(original) # still [4, 5, 6] - assignment didn't change it -
Not understanding when objects are modified
# BAD - thinking this creates a new object
@dataclass
class Person:
name: str
age: int
person = Person("Alice", 25)
person.age = 26 # modifies the existing person object
2. Multiple Variables Pointing to Same Data
What to Teach
When multiple variables reference the same object, mutations through any variable affect all variables. This is called aliasing and can lead to unexpected behavior.
Aliasing with Lists
# two variables pointing to the same list
list1 = [1, 2, 3]
list2 = list1 # list2 is an alias for list1 - same object
# modifying through either variable affects both
list1.append(4)
print(list1) # [1, 2, 3, 4]
print(list2) # [1, 2, 3, 4] - same object
# assignment only affects one variable
list1 = [10, 20, 30] # list1 now points to new object
print(list1) # [10, 20, 30]
print(list2) # [1, 2, 3, 4] - still points to original object
What to Point Out:
- Multiple variables can reference the same object
- Mutations through any alias affect all aliases
- Assignment breaks the alias relationship
Common Student Mistakes to Watch For
-
Not expecting aliasing behavior
original = [1, 2, 3]
copy = original # think this creates a copy, but it's an alias
copy.append(4)
print(original) # [1, 2, 3, 4] - original was modified -
Thinking assignment affects all aliases
# BAD - expecting assignment to affect original
def broken_clear(data: list[int]) -> None:
data = [] # assignment - doesn't affect original
my_list = [1, 2, 3]
broken_clear(my_list)
print(my_list) # still [1, 2, 3] - assignment didn't clear it -
Creating unintended aliases
# BAD - creating aliases when copies were intended
students = [
Student("Alice", 3.5),
Student("Bob", 3.2)
]
# creates aliases, not copies
honor_students = students # same list object!
honor_students.append(Student("Charlie", 3.8))
print(len(students)) # 3 - original list was modified
3. Consequences of Aliasing
What to Teach
Aliasing can lead to unexpected side effects, bugs, and hard-to-debug code. Students need to understand when aliasing happens and how to avoid problems.
Unintended Side Effects
def process_grades(grades: list[float]) -> list[float]:
# BAD - modifies original list
grades.sort() # mutation - affects original
# remove failing grades
passing_grades = []
for grade in grades:
if grade >= 60:
passing_grades.append(grade)
return passing_grades
original_grades = [85.0, 55.0, 92.0, 78.0]
processed = process_grades(original_grades)
print(f"Original: {original_grades}") # [55.0, 78.0, 85.0, 92.0] - sorted
print(f"Processed: {processed}") # [78.0, 85.0, 92.0]
Teaching Examples
Fixing Side Effects:
def process_grades_safely(grades: list[float]) -> list[float]:
"""Process grades without modifying original."""
# GOOD - create a copy first
grades_copy = grades.copy()
grades_copy.sort()
passing_grades = []
for grade in grades_copy:
if grade >= 60:
passing_grades.append(grade)
return passing_grades
original_grades = [85.0, 55.0, 92.0, 78.0]
processed = process_grades_safely(original_grades)
print(f"Original: {original_grades}") # [85.0, 55.0, 92.0, 78.0] - unchanged
print(f"Processed: {processed}") # [78.0, 85.0, 92.0]
What to Point Out:
- Mutations can have unexpected side effects on aliased objects
- Debugging aliasing bugs can be difficult
- Always consider whether you need a copy or can work with the original
4. Safe Patterns and Best Practices
What to Teach
Students need to learn patterns that avoid aliasing problems and make code more predictable and maintainable.
Creating Copies When Needed
# list copying methods
original = [1, 2, 3]
copy1 = original.copy() # shallow copy
copy2 = list(original) # shallow copy
copy3 = original[:] # shallow copy
# dataclass copying
from dataclasses import replace
@dataclass
class Person:
name: str
age: int
original_person = Person("Alice", 25)
copied_person = replace(original_person) # creates copy
modified_person = replace(original_person, age=26) # copy with changes
recreated_person = Person(original_person.name, 27) # strings immutable, fine to alias
Defensive Programming
def safe_list_processor(data: list[int]) -> dict[str, int]:
"""process list data safely without side effects"""
# create copy to avoid modifying original
work_data = data.copy()
# now safe to modify work_data
work_data.sort()
return {
"min": work_data[0] if work_data else 0,
"max": work_data[-1] if work_data else 0,
"count": len(work_data)
}
def safe_dataclass_processor(student: Student) -> dict[str, any]:
"""process student data without modifying original"""
# work with fields directly - don't modify object
return {
"name_length": len(student.name),
"gpa_letter": "A" if student.gpa >= 3.5 else "B",
"honors": student.gpa >= 3.7
}
Common Student Mistakes to Watch For
-
Forgetting to copy when needed
# BAD - will modify original
def bad_sort(items: list[int]) -> list[int]:
items.sort() # modifies original
return items
# FIX - works with copy
def good_sort(items: list[int]) -> list[int]:
return sorted(items) # returns new sorted list -
Making unnecessary copies
# BAD - unnecessary copy when only reading
def bad_find_max(items: list[int]) -> int:
items_copy = items.copy() # unnecessary - just reading
return max(items_copy)
# FIX - no copy needed for read-only operations
def good_find_max(items: list[int]) -> int:
return max(items) # safe to read original
Common Teaching Scenarios
When Students Ask "Why does changing one variable affect another?"
"Both variables are pointing to the same object in memory. When you modify the object (mutation), all variables pointing to it see the change. If you assign a new object to one variable, only that variable changes."
When Students Get Unexpected Side Effects
"Check if your function is modifying its parameters. If you need to change data, create a copy first with .copy() for lists or recreate (or use .replace() with for dataclasses."
When Students Are Confused About Assignment vs Mutation
"Assignment (=) changes what a variable points to. Mutation (.append(), .field = value) changes the contents of the object the variable points to."