1

Debug a stats pipeline

Try the time-travel debugger

The program below builds a list of student records, scores them, and computes a simple grade distribution. It calls eight different functions, mutates state across them, and prints a summary at the end.

It works correctly — there’s no bug to find. The point is to explore the debugger.

Things to try

  1. A breakpoint is already set in the gutter on the score line. Click in the gutter to add, remove, or move breakpoints.
  2. Click Debug (next to Run). Execution pauses at your breakpoint — the Variables tab shows the current record and locals.
  3. Use Step Into (F11) to dive into compute_score. Watch the Call Stack tab grow. Click frames to inspect each.
  4. Use Step Over (F10) a few times, then hit Step Back (Shift+F10) to rewind. Notice that the debugger is no longer in the live execution state; sighted users also see the gutter marker change appearance.
  5. Open the Watch tab and add len(records) and sum(scores) / max(len(scores), 1).
  6. Hover over any identifier in the editor while paused — the value appears in a tooltip.
  7. Add a conditional breakpoint on the loop body: right-click the breakpoint marker in the gutter and enter record["grade"] == "A".
  8. Drag the History scrubber to navigate the entire execution timeline.

Parameters experiment

The Output panel’s args: input is enabled for this demo. Try 3 5: the first parameter limits the roster to three students, and the second adds a five-point curve before grades are assigned. Run and Debug should receive the same sys.argv.

Aliasing experiment

The function register_student deliberately appends to a shared default list — a classic Python pitfall. After running, expand defaults and students in the Variables tab — they share the same oid, and you’ll see a alias badge.

Starter files
stats_pipeline.py
# Student grade pipeline — exercises the debugger across many calls.

import sys

def make_record(name, raw_scores):
    return {"name": name, "raw_scores": raw_scores, "grade": None}

def parse_parameters(argv):
    limit = None
    curve = 0.0
    if len(argv) > 1 and argv[1] != "all":
        limit = int(argv[1])
    if len(argv) > 2:
        curve = float(argv[2])
    return limit, curve

def compute_score(record):
    total = sum(record["raw_scores"])
    return total / len(record["raw_scores"])

def assign_grade(score):
    if score >= 90: return "A"
    if score >= 80: return "B"
    if score >= 70: return "C"
    if score >= 60: return "D"
    return "F"

def grade_distribution(records):
    dist = {"A": 0, "B": 0, "C": 0, "D": 0, "F": 0}
    for r in records:
        dist[r["grade"]] += 1
    return dist

def print_distribution(dist):
    total = sum(dist.values())
    for g in ["A", "B", "C", "D", "F"]:
        bar = "#" * dist[g]
        pct = 100 * dist[g] / max(total, 1)
        print(f"  {g}: {bar:<10} ({dist[g]}, {pct:.1f}%)")

def average(scores):
    return sum(scores) / len(scores) if scores else 0.0

# Classic aliasing pitfall: the default list is shared across calls
# because it's evaluated once at function-definition time.
def register_student(name, raw_scores, students=[]):
    record = make_record(name, raw_scores)
    students.append(record)
    return students

def main():
    limit, curve = parse_parameters(sys.argv)
    roster = [
        ("Ada",     [95, 88, 92]),
        ("Linus",   [72, 81, 78]),
        ("Grace",   [98, 95, 91]),
        ("Alan",    [60, 55, 70]),
        ("Margaret",[85, 89, 87]),
    ]
    if limit is not None:
        roster = roster[:limit]
    records = []
    scores = []
    defaults = []   # what register_student returns first time
    for name, raw in roster:
        defaults = register_student(name, raw)
        record = defaults[-1]
        score = min(compute_score(record) + curve, 100)
        record["grade"] = assign_grade(score)
        scores.append(score)
        records.append(record)

    print(f"Parameters: limit={limit}, curve={curve}")
    print(f"Average: {average(scores):.2f}")
    print("Grade distribution:")
    print_distribution(grade_distribution(records))
    return records

main()