Case Study: From Spaghetti Code to Clean Functions

This case study uses a fictional but realistic scenario. The code patterns shown — and the problems they create — are drawn from real codebases that the author and colleagues have encountered in production environments.

The Scenario

Marcus is a teaching assistant for an introductory biology lab. Each week, he receives a CSV-like text dump of student lab scores and needs to produce a summary: each student's average, their letter grade, and overall class statistics. He wrote a Python script during his first semester as a TA, and it works — most of the time.

Here's what Marcus wrote:

# Marcus's lab grade processor — version 1 ("the blob")
scores_text = """Alice,85,92,78,90
Bob,72,68,74,80
Charlie,95,98,92,97
Diana,60,55,72,65
Eve,88,91,85,89"""

lines = scores_text.strip().split("\n")
all_averages = []
print("=" * 50)
print("BIOLOGY LAB GRADE REPORT")
print("=" * 50)
for line in lines:
    parts = line.split(",")
    name = parts[0]
    total = 0
    count = 0
    for i in range(1, len(parts)):
        total += int(parts[i])
        count += 1
    average = total / count
    all_averages.append(average)
    if average >= 90:
        grade = "A"
    elif average >= 80:
        grade = "B"
    elif average >= 70:
        grade = "C"
    elif average >= 60:
        grade = "D"
    else:
        grade = "F"
    print(f"{name}: Average = {average:.1f}, Grade = {grade}")
print("-" * 50)
class_total = 0
for a in all_averages:
    class_total += a
class_average = class_total / len(all_averages)
highest = all_averages[0]
lowest = all_averages[0]
for a in all_averages:
    if a > highest:
        highest = a
    if a < lowest:
        lowest = a
if class_average >= 90:
    class_grade = "A"
elif class_average >= 80:
    class_grade = "B"
elif class_average >= 70:
    class_grade = "C"
elif class_average >= 60:
    class_grade = "D"
else:
    class_grade = "F"
print(f"Class Average: {class_average:.1f} ({class_grade})")
print(f"Highest Average: {highest:.1f}")
print(f"Lowest Average: {lowest:.1f}")
print("=" * 50)

The script produces this output:

==================================================
BIOLOGY LAB GRADE REPORT
==================================================
Alice: Average = 86.2, Grade = B
Bob: Average = 73.5, Grade = C
Charlie: Average = 95.5, Grade = A
Diana: Average = 63.0, Grade = D
Eve: Average = 88.2, Grade = B
--------------------------------------------------
Class Average: 81.3 (B)
Highest Average: 95.5
Lowest Average: 63.0
==================================================

It works. So what's wrong with it?

The Problems

Problem 1: The Grading Logic Appears Twice

Look carefully at the code. The if/elif chain for determining a letter grade appears in two places — once for individual students and once for the class average. If the biology department changes the grading scale (say, adding A+ for averages above 97), Marcus has to find and update both copies. And if he misses one? The individual grades and the class grade use different scales. This is a real bug waiting to happen.

Problem 2: Everything Is Entangled

The parsing logic (splitting CSV data), the computation logic (calculating averages), the grading logic, and the display logic are all woven together in one block. You can't reuse any piece independently. If Marcus's chemistry professor asks him to compute averages for a different dataset, he can't just grab the averaging code — it's tangled up with the parsing and printing code.

Problem 3: It's Hard to Read

Try to answer this question quickly: what does line 14 do? You can't — you have to trace through the code from the top to understand the context. There are no meaningful boundaries or labels.

Problem 4: It's Impossible to Test

How would Marcus verify that the grading logic is correct for edge cases (like exactly 90.0, or exactly 59.999)? He'd have to construct entire CSV strings and run the whole script. There's no way to test just the grading part.

The Refactoring

Let's carve this monolith into functions. The key question at each step: "What is this chunk of code's single responsibility?"

"""Lab Grade Report Generator — Refactored with functions.

Processes student lab scores and produces a formatted grade report
with individual and class-level statistics.
"""


def parse_scores(scores_text):
    """Parse a multi-line string of scores into a list of (name, scores) tuples.

    Parameters:
        scores_text (str): Multi-line string where each line is
                          "Name,score1,score2,..."

    Returns:
        list: List of tuples like [("Alice", [85, 92, 78, 90]), ...]
    """
    students = []
    for line in scores_text.strip().split("\n"):
        parts = line.split(",")
        name = parts[0].strip()
        scores = [int(s.strip()) for s in parts[1:]]
        students.append((name, scores))
    return students


def calculate_average(scores):
    """Return the average of a list of numeric scores.

    Parameters:
        scores (list): A non-empty list of numbers.

    Returns:
        float: The arithmetic mean of the scores.
    """
    if not scores:
        return 0.0
    return sum(scores) / len(scores)


def determine_grade(average):
    """Return the letter grade for a numeric average.

    Parameters:
        average (float): A numeric score average.

    Returns:
        str: A letter grade ("A", "B", "C", "D", or "F").
    """
    if average >= 90:
        return "A"
    elif average >= 80:
        return "B"
    elif average >= 70:
        return "C"
    elif average >= 60:
        return "D"
    else:
        return "F"


def calculate_class_stats(averages):
    """Return class-level statistics from a list of student averages.

    Parameters:
        averages (list): A list of numeric averages.

    Returns:
        tuple: (class_average, highest, lowest)
    """
    class_avg = calculate_average(averages)
    return class_avg, max(averages), min(averages)


def print_report(students_data):
    """Print a formatted grade report for all students.

    Parameters:
        students_data (list): List of (name, scores) tuples.
    """
    print("=" * 50)
    print("BIOLOGY LAB GRADE REPORT")
    print("=" * 50)

    averages = []
    for name, scores in students_data:
        avg = calculate_average(scores)
        grade = determine_grade(avg)
        averages.append(avg)
        print(f"{name}: Average = {avg:.1f}, Grade = {grade}")

    print("-" * 50)
    class_avg, highest, lowest = calculate_class_stats(averages)
    class_grade = determine_grade(class_avg)
    print(f"Class Average: {class_avg:.1f} ({class_grade})")
    print(f"Highest Average: {highest:.1f}")
    print(f"Lowest Average: {lowest:.1f}")
    print("=" * 50)


def main():
    """Process lab scores and display the grade report."""
    scores_text = """Alice,85,92,78,90
Bob,72,68,74,80
Charlie,95,98,92,97
Diana,60,55,72,65
Eve,88,91,85,89"""

    students = parse_scores(scores_text)
    print_report(students)


if __name__ == "__main__":
    main()

What Changed — and Why It Matters

1. The Grading Logic Exists in Exactly One Place

determine_grade() is called twice — once for individual students and once for the class average — but the logic is defined only once. If the grading scale changes, you update one function and both uses get the update automatically. This is the DRY principle in action.

2. Each Function Has a Clear, Single Responsibility

Function Responsibility
parse_scores() Convert raw text into structured data
calculate_average() Compute an average
determine_grade() Map a number to a letter grade
calculate_class_stats() Compute aggregate statistics
print_report() Format and display the report
main() Orchestrate the entire process

3. Functions Are Independently Reusable

Marcus can now reuse calculate_average() in his chemistry grading script. He can reuse determine_grade() for any course that uses the same scale. He can reuse parse_scores() for any CSV-like data. The functions are building blocks, not a monolithic blob.

4. Reading main() Tells the Story

Look at main() — it's two lines of actual logic: 1. Parse the scores 2. Print the report

You can understand what the program does without understanding how any individual function works. That's abstraction.

5. Testing Becomes Possible

Now Marcus can test the grading logic independently:

# Quick smoke tests
assert determine_grade(95) == "A"
assert determine_grade(90) == "A"   # Boundary
assert determine_grade(89.9) == "B"  # Just below boundary
assert determine_grade(59) == "F"
assert calculate_average([80, 90, 100]) == 90.0
assert calculate_average([]) == 0.0
print("All tests passed!")
All tests passed!

Discussion Questions

  1. The original script had the grading logic duplicated. In a large codebase with hundreds of files, how would duplicated logic become even more dangerous? What happens when a team of five developers each copies and modifies the same logic?

  2. Marcus's calculate_average() function would crash if given an empty list (division by zero). The refactored version handles this by returning 0.0. Is this the right choice? What alternatives can you think of? (Hint: consider what happens if the caller doesn't expect 0.0 as a valid average.)

  3. The parse_scores() function assumes the data is well-formatted. What could go wrong with real-world data? List at least three things that could cause the function to crash. How might you handle each one? (We'll address this formally in Chapter 11 on error handling.)

  4. Look at the function print_report(). It both computes (calculating averages) and displays (printing). Does this violate the single responsibility principle? How might you split it further?

  5. The refactored version is more lines of code than the original. Does more code always mean better code? When is conciseness a virtue and when is it a trap?

Mini-Project

Take a script you wrote for a previous chapter's exercises — or any script you've written elsewhere. Refactor it into functions. Follow these steps:

  1. Read through the script and identify distinct "chunks" of functionality.
  2. Give each chunk a function name that describes what it does (verb + noun).
  3. Identify what data each chunk needs (these become parameters) and what it produces (this becomes the return value).
  4. Write the functions, then write a main() function that calls them in order.
  5. Verify that the refactored version produces exactly the same output as the original.

Write a brief paragraph comparing the before and after versions: what improved? Was anything harder in the refactored version?

References

  • This case study is a Tier 3 illustrative example. The scenario is fictional, but the code patterns — duplicated logic, entangled concerns, untestable monoliths — are among the most common problems in real-world codebases.
  • Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall. Chapter 3 ("Functions") covers function design principles extensively. (Tier 1)
  • The DRY principle was coined by Hunt, A. & Thomas, D. (1999). The Pragmatic Programmer. Addison-Wesley. (Tier 1)