Skip to main content

Practice Problems: Aliasing & Mutation in Python

Solutions are found at the bottom of the page!

Problem 1: Understanding Assignment vs Mutation

Part A: Code Prediction Exercises For each code snippet, predict what will be printed and explain why:

# Exercise 1: List Assignment vs Mutation
def exercise_1():
list1 = [1, 2, 3]
list2 = list1

# Scenario A: Assignment
list1 = [4, 5, 6]
print("After assignment:")
# Predict what will be printed:
print(f"list1: {list1}")
print(f"list2: {list2}")
# Reset
list1 = [1, 2, 3]
list2 = list1

# Scenario B: Mutation
list1.append(4)
print("After mutation:")
# Predict what will be printed:
print(f"list1: {list1}")
print(f"list2: {list2}")

# Exercise 2: Dataclass Fields
from dataclasses import dataclass

@dataclass
class Person:
name: str
age: int

def exercise_2():
person1 = Person("Alice", 25)
person2 = person1

# Scenario A: Assignment
person1 = Person("Bob", 30)
print("After assignment:")
# Predict what will be printed:
print(f"person1: {person1}")
print(f"person2: {person2}")

# Reset
person1 = Person("Alice", 25)
person2 = person1

# Scenario B: Field mutation
person1.age = 26
print("After field mutation:")
# Predict what will be printed:
print(f"person1: {person1}")
print(f"person2: {person2}")

if __name__ == "__main__":
exercise_1()
exercise_2()

Problem 2: Identifying Aliasing Problems

Tasks: Fix the following functions that have aliasing-related bugs:

from dataclasses import dataclass
from typing import List

@dataclass
class Student:
name: str
grades: List[int]
gpa: float

# Problem Function 1: Unintended list sharing
def create_students_v1(names: List[str]) -> List[Student]:
"""create student objects with empty grade lists"""
default_grades = [] # What's the problem here?
students = []

for name in names:
student = Student(name, default_grades, 0.0)
students.append(student)

return students

def test_create_students_v1():
"""test that demonstrates the aliasing problem"""
students = create_students_v1(["Alice", "Bob", "Charlie"])

# Add a grade to Alice
students[0].grades.append(95)

# What happens to other students' grades?
print("Alice's grades:", students[0].grades)
print("Bob's grades:", students[1].grades)
print("Charlie's grades:", students[2].grades)

# Fix this function!

# Problem Function 2: Modifying input parameters
def process_scores(student: Student, new_scores: List[int]) -> Student:
"""process and add new scores to a student"""
# This modifies the original student
student.grades.extend(new_scores)
student.gpa = sum(student.grades) / len(student.grades)
return student

def test_process_scores():
"""test that demonstrates parameter modification"""
original = Student("Dave", [85, 90], 87.5)
print("Original before:", original)

processed = process_scores(original, [95, 88])
print("Original after:", original)
print("Processed:", processed)

# Fix this function to not modify the original!

# Your task: Fix all these functions and write corrected versions
if __name__ == "__main__":
test_create_students_v1_fixed()
test_process_scores_fixed()

Solutions

Problem 1:

Predicted Outputs and Explanations:

from dataclasses import dataclass

def exercise_1():
list1 = [1, 2, 3]
list2 = list1 # Creates an alias - both variables point to the same list object

# Scenario A: Assignment
list1 = [4, 5, 6] # Changes what list1 points to, doesn't affect list2
print("After assignment:")
print(f"list1: {list1}") # [4, 5, 6]
print(f"list2: {list2}") # [1, 2, 3] - still points to original list

# Reset
list1 = [1, 2, 3]
list2 = list1 # Both point to the same list object again

# Scenario B: Mutation
list1.append(4) # Modifies the list object that both variables point to
print("After mutation:")
print(f"list1: {list1}") # [1, 2, 3, 4]
print(f"list2: {list2}") # [1, 2, 3, 4] - same object, so sees the change

@dataclass
class Person:
name: str
age: int

def exercise_2():
person1 = Person("Alice", 25)
person2 = person1 # Creates an alias - both variables point to the same Person object

# Scenario A: Assignment
person1 = Person("Bob", 30) # Changes what person1 points to
print("After assignment:")
print(f"person1: {person1}") # Person(name='Bob', age=30)
print(f"person2: {person2}") # Person(name='Alice', age=25) - still points to original

# Reset
person1 = Person("Alice", 25)
person2 = person1 # Both point to the same Person object again

# Scenario B: Field mutation
person1.age = 26 # Modifies the Person object that both variables point to
print("After field mutation:")
print(f"person1: {person1}") # Person(name='Alice', age=26)
print(f"person2: {person2}") # Person(name='Alice', age=26) - same object

if __name__ == "__main__":
exercise_1()
exercise_2()

Problem 2:

from dataclasses import dataclass
from typing import List

@dataclass
class Student:
name: str
grades: List[int]
gpa: float

# Fixed Function 1: Avoid shared mutable objects
def create_students_v1_fixed(names: List[str]) -> List[Student]:
"""create student objects with independent empty grade lists"""
students = []

for name in names:
# Create a new empty list for each student
student = Student(name, [], 0.0)
students.append(student)

return students

def test_create_students_v1_fixed():
"""test that students have independent grade lists"""
students = create_students_v1_fixed(["Alice", "Bob", "Charlie"])

# Add a grade to Alice
students[0].grades.append(95)

# Other students should not be affected
assert students[0].grades == [95], "Alice should have the grade"
assert students[1].grades == [], "Bob should have empty grades"
assert students[2].grades == [], "Charlie should have empty grades"
print("Students have independent grade lists")

# Fixed Function 2: Don't modify input parameters
def process_scores_fixed(student: Student, new_scores: List[int]) -> Student:
"""process and add new scores to a student without modifying the original"""
# Create new grade list with combined scores
updated_grades = student.grades + new_scores # Creates new list
new_gpa = sum(updated_grades) / len(updated_grades) if updated_grades else 0.0

# Return new student object with updated data
# strings are immutable, so okay to alias student.name
return Student(student.name, updated_grades, new_gpa)

def test_process_scores_fixed():
"""test that original student is not modified"""
original = Student("Dave", [85, 90], 87.5)
original_grades_copy = original.grades.copy()

processed = process_scores_fixed(original, [95, 88])

# Original should be unchanged
assert original.grades == original_grades_copy, "Original grades should be unchanged"
assert original.gpa == 87.5, "Original GPA should be unchanged"

# Processed should have new data
assert processed.grades == [85, 90, 95, 88], "Processed should have all grades"
assert processed.gpa == 89.5, "Processed should have updated GPA"
print("Original student not modified")

if __name__ == "__main__":
test_create_students_v1_fixed()
test_process_scores_fixed()