Case Study: Building a Class Roster Manager
The Scenario
Professor Kim teaches three sections of Introduction to Biology. At the start of every semester, she juggles add/drop requests, waitlists, and section transfers — all tracked in a spreadsheet that she copy-pastes into various forms by hand. She's asked you, a student worker in the department, to build a simple Python tool that manages her class rosters.
The requirements are straightforward:
- Maintain a roster (list of students) for each section
- Add and remove students
- Transfer students between sections
- Check if a student is enrolled in any section
- Generate a combined roster sorted alphabetically
- Enforce a maximum class size of 30 students per section
This is a perfect use case for lists: the data is ordered, it changes frequently (add/drop), and the operations are all standard list manipulations.
The Initial Design
We'll represent each section as a list of student names, and store all three sections in a dictionary mapping section numbers to their rosters. (We're previewing dictionaries slightly — think of it as a labeled collection of lists.)
# Initial roster setup
sections = {
101: ["Adams, J.", "Chen, L.", "Garcia, M.", "Kim, S.", "Okafor, N."],
102: ["Baker, R.", "Davis, T.", "Huang, W.", "Lopez, A.", "Patel, D."],
103: ["Brown, K.", "Evans, C.", "Jones, P.", "Miller, H.", "Singh, R."],
}
MAX_CAPACITY = 30
Adding a Student
The first operation: adding a student to a section. We need to check two things — is the section full, and is the student already enrolled?
def add_student(sections: dict, section_num: int, student: str) -> bool:
"""Add a student to a section. Returns True if successful."""
if section_num not in sections:
print(f"Error: Section {section_num} does not exist.")
return False
roster = sections[section_num]
if len(roster) >= MAX_CAPACITY:
print(f"Section {section_num} is full ({MAX_CAPACITY} students).")
return False
if student in roster:
print(f"{student} is already in section {section_num}.")
return False
roster.append(student)
roster.sort() # Keep alphabetical order
print(f"Added {student} to section {section_num}.")
return True
# Test it
add_student(sections, 101, "Franklin, B.")
# Output: Added Franklin, B. to section 101.
add_student(sections, 101, "Chen, L.")
# Output: Chen, L. is already in section 101.
add_student(sections, 999, "Nobody, X.")
# Output: Error: Section 999 does not exist.
Key list operations used: len() for capacity check, in for membership check, .append() to add, .sort() to maintain order.
Design note: Notice that
roster = sections[section_num]creates an alias, not a copy. When we callroster.append(student), it modifies the list insidesectionsdirectly. This is intentional here — we want the changes to persist. This is exactly the aliasing behavior from section 8.8, working in our favor.
Removing a Student
Dropping a student requires finding them first:
def remove_student(sections: dict, section_num: int, student: str) -> bool:
"""Remove a student from a section. Returns True if successful."""
if section_num not in sections:
print(f"Error: Section {section_num} does not exist.")
return False
roster = sections[section_num]
if student not in roster:
print(f"{student} is not in section {section_num}.")
return False
roster.remove(student)
print(f"Removed {student} from section {section_num}.")
return True
# Test it
remove_student(sections, 101, "Garcia, M.")
# Output: Removed Garcia, M. from section 101.
remove_student(sections, 101, "Nobody, X.")
# Output: Nobody, X. is not in section 101.
Key list operation used: .remove() to delete by value. We check with in first to avoid the ValueError that .remove() raises when the element isn't found.
Transferring Between Sections
A transfer is a remove-then-add operation, but it needs to be atomic — if the target section is full, we shouldn't remove the student from their current section:
def transfer_student(sections: dict, student: str,
from_section: int, to_section: int) -> bool:
"""Transfer a student between sections. Returns True if successful."""
if from_section not in sections or to_section not in sections:
print("Error: Invalid section number.")
return False
if student not in sections[from_section]:
print(f"{student} is not in section {from_section}.")
return False
if len(sections[to_section]) >= MAX_CAPACITY:
print(f"Section {to_section} is full. Transfer denied.")
return False
if student in sections[to_section]:
print(f"{student} is already in section {to_section}.")
return False
# Safe to transfer — check everything before modifying anything
sections[from_section].remove(student)
sections[to_section].append(student)
sections[to_section].sort()
print(f"Transferred {student}: section {from_section} -> {to_section}.")
return True
# Test it
transfer_student(sections, "Baker, R.", 102, 101)
# Output: Transferred Baker, R.: section 102 -> 101.
Design principle: Validate everything before modifying anything. If we had removed the student first and then discovered the target section was full, we'd have a student who's in neither section. This "validate first, modify second" approach prevents data corruption.
Finding a Student Across All Sections
Sometimes Professor Kim needs to find which section a student is in:
def find_student(sections: dict, student: str) -> int | None:
"""Find which section a student is in. Returns section number or None."""
for section_num, roster in sections.items():
if student in roster:
return section_num
return None
# Test it
result = find_student(sections, "Singh, R.")
if result is not None:
print(f"Singh, R. is in section {result}.")
else:
print("Student not found in any section.")
# Output: Singh, R. is in section 103.
Key list operation used: in for membership testing — used inside a loop over all sections.
Generating Reports
The combined roster uses list concatenation and sorting:
def combined_roster(sections: dict) -> list[str]:
"""Return a sorted list of all students across all sections."""
all_students = []
for roster in sections.values():
all_students.extend(roster)
all_students.sort()
return all_students
def section_summary(sections: dict) -> None:
"""Print a summary of all sections."""
print("\n=== Section Summary ===")
total = 0
for section_num in sorted(sections.keys()):
roster = sections[section_num]
count = len(roster)
total += count
print(f"Section {section_num}: {count}/{MAX_CAPACITY} students")
for i, student in enumerate(roster, start=1):
print(f" {i:2}. {student}")
print(f"\nTotal enrolled: {total}")
combined = combined_roster(sections)
print(f"Unique students: {len(combined)}")
# Test it
section_summary(sections)
Output:
=== Section Summary ===
Section 101: 5/30 students
1. Adams, J.
2. Baker, R.
3. Chen, L.
4. Franklin, B.
5. Kim, S.
Section 102: 4/30 students
1. Davis, T.
2. Huang, W.
3. Lopez, A.
4. Patel, D.
Section 103: 5/30 students
1. Brown, K.
2. Evans, C.
3. Jones, P.
4. Miller, H.
5. Singh, R.
Total enrolled: 14
Unique students: 14
The Aliasing Lesson
Early in development, a subtle bug appeared. Professor Kim wanted a "snapshot" of the roster at the start of the semester to compare against the final roster:
# BUG: This creates an alias, not a snapshot!
start_of_semester = sections[101]
# ... later, after add/drop ...
print(start_of_semester) # Shows the CURRENT roster, not the original!
Because start_of_semester = sections[101] creates an alias, every add/drop modification to section 101 also changes start_of_semester. The fix:
# FIXED: Create a copy
start_of_semester = sections[101].copy()
Now start_of_semester is an independent list. End-of-semester comparisons work correctly.
Discussion Questions
-
The
transfer_studentfunction validates all conditions before making any changes. Why is this important? What could go wrong with a "remove first, then check if add is possible" approach? -
The roster is kept sorted after every add operation using
.sort(). What are the tradeoffs of sorting after every insertion vs. sorting only when displaying? (Hint: think about how often each operation happens.) -
We used
student in rosterto check membership, which scans the entire list. If Professor Kim's sections had 500 students each, this would be slow. What data structure (from Chapter 9) would make membership testing faster? What tradeoff would that involve? -
The aliasing bug with
start_of_semesterwas caught during testing. How would you design a test to catch this kind of bug automatically? What would you check? -
If Professor Kim wanted to track more information per student (student ID, email, major), would a list of strings still be the right data structure? What would you use instead? (Preview of Chapters 9 and 14.)