Skip to main content

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

  1. 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
  2. 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
  3. 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

  1. 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
  2. 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
  3. 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

  1. 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
  2. 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."