Skip to main content

Capstone: build a real CLI tool end to end

You've learned every piece. This lesson is where they snap together into one real program. Reading and quizzing prove you understand; building a complete thing proves you can make. We're going to build a small command-line tool — a program you run in the terminal that reads a little JSON data file, processes it, and prints a tidy report. It's modest on purpose, but it touches everything: functions, a class, JSON, comprehensions, files, and error handling, all cooperating. Finish it and you've genuinely crossed from "I can write snippets" to "I can build a program."

What we're building

A grade report tool. It reads a JSON file of students and their test scores, and prints a report: each student's average and letter grade, plus a class summary. Here's the target output:

=== Grade Report ===
Ada avg 87.5 B
Lin avg 91.0 A
Sam avg 68.0 D

Class average: 82.2
Top student: Lin (91.0)

And the input data file (grades.json):

{
"students": [
{"name": "Ada", "scores": [90, 85]},
{"name": "Lin", "scores": [88, 94]},
{"name": "Sam", "scores": [60, 76]}
]
}

We'll build it in five steps, each with acceptance criteria — a concrete check that the step works before you move on. That step-by-step, verify-as-you-go rhythm is exactly the problem-solving loop from the debugging lesson, applied to a whole project.

:::note How to work through this Each step below has a runnable challenge so you can build and check the piece right here in the browser. The challenges use JSON strings and Pyodide's in-browser file system, so everything runs without the network. At the end, the "Put it together" section shows the complete program as you'd save it in a real .py file and run with python grades.py — the local-Python skills from the last lesson. :::

Step 1 — A function to score one student

Start with the smallest piece: given one student's list of scores, compute the average. Build the leaf functions first, then assemble upward — small pieces are easy to test.

Acceptance criteria: average([90, 85]) returns 87.5; average([60, 76]) returns 68.0; an empty list returns 0 (no crash on division by zero).

⌨️ Challenge — Python · practice (not graded)

Write average(scores) that returns the mean of a list of numbers as a float. If the list is empty, return 0 (don't divide by zero). average([90, 85]) is 87.5; average([]) is 0.

Step 2 — A function for the letter grade

Now turn an average into a letter, using the decision skills from the conditionals lesson.

Acceptance criteria: 90+ → "A", 80–89 → "B", 70–79 → "C", 60–69 → "D", below 60 → "F". Boundaries land on the higher grade (exactly 90 is an "A").

⌨️ Challenge — Python · practice (not graded)

Write letter_grade(avg) that returns a letter: "A" for 90 and up, "B" for 80–89, "C" for 70–79, "D" for 60–69, "F" below 60. letter_grade(87.5) is "B"; letter_grade(90) is "A".

Step 3 — A class to model a student

Bundle a student's data and behavior into an object, the way the OOP lesson taught. The class will use your two functions.

Acceptance criteria: Student("Ada", [90, 85]) has .name == "Ada"; calling .average() returns 87.5 and .grade() returns "B".

⌨️ Challenge — Python · practice (not graded)

Define a class Student whose __init__ stores self.name and self.scores. Give it a method average() returning the mean of its scores (0 if empty), and grade() returning the letter ("A">=90, "B">=80, "C">=70, "D">=60, else "F"). Then check_student(name, scores) creates a Student and returns the list [student.name, student.average(), student.grade()].

Step 4 — Parse the JSON and build the report

Now wire in the data. Parse the JSON string into Python objects, build a Student for each record (a comprehension is perfect here), and assemble the report data.

Acceptance criteria: given the JSON string, build_report(text) returns a list of [name, average, grade] rows, one per student, in order.

⌨️ Challenge — Python · practice (not graded)

Write build_report(text). Parse the JSON string (shape: {"students": [{"name": ..., "scores": [...]}, ...]}) with json.loads, build a Student for each, and return a list of rows [name, average, grade] in order. Reuse the Student class. For the sample data, the first row is ["Ada", 87.5, "B"].

Step 5 — Read from a file, with error handling

Real tools read a file, and real files are sometimes missing or malformed. Wrap the read in try/except so the tool fails gracefully instead of crashing — the files-and-exceptions skill.

Acceptance criteria: load_report("grades.json") returns the report rows when the file exists and is valid; if the file is missing it returns the string "error: file not found"; if the file holds invalid JSON it returns "error: bad data".

⌨️ Challenge — Python · practice (not graded)

Write run_capstone(filename, text_to_write). If text_to_write is not None, first write it to `filename` (so we can test). Then try to: open and read `filename`, json.loads it, and return build_report of it. If the file is missing return "error: file not found" (catch FileNotFoundError); if the JSON is invalid return "error: bad data" (catch json.JSONDecodeError). build_report and Student are provided.

Put it together: the complete program

Here is the whole tool as one file, the way you'd save and run it locally. Combine the pieces you just built, add the printing and class-summary logic, and you have a real CLI tool:

# grades.py — run with: python grades.py
import json


class Student:
def __init__(self, name, scores):
self.name = name
self.scores = scores

def average(self):
if len(self.scores) == 0:
return 0
return sum(self.scores) / len(self.scores)

def grade(self):
avg = self.average()
if avg >= 90:
return "A"
elif avg >= 80:
return "B"
elif avg >= 70:
return "C"
elif avg >= 60:
return "D"
return "F"


def load_students(filename):
"""Read the JSON file and return a list of Student objects."""
with open(filename, "r") as f:
data = json.loads(f.read())
return [Student(s["name"], s["scores"]) for s in data["students"]]


def print_report(students):
print("=== Grade Report ===")
for s in students:
# f-strings (you've seen these in the strings lesson) make tidy columns
print(f"{s.name:<8} avg {s.average():<6} {s.grade()}")

averages = [s.average() for s in students]
class_avg = sum(averages) / len(averages)
top = max(students, key=lambda s: s.average()) # the highest-average student
print()
print(f"Class average: {class_avg:.1f}")
print(f"Top student: {top.name} ({top.average()})")


def main():
try:
students = load_students("grades.json")
except FileNotFoundError:
print("error: grades.json not found")
return
except json.JSONDecodeError:
print("error: grades.json is not valid JSON")
return
print_report(students)


main()

To run it for real (using the local-Python skills from the last lesson): make a folder, create grades.json with the sample data and grades.py with this code, then in the terminal run python grades.py. You'll see the report printed. Tweak the data, run again, watch it update — that's the edit-run loop on your own machine.

:::tip What's new and what's not The only genuinely new bits above are convenience touches, not new concepts: f-strings like f"{s.name:<8}" (a tidier way to format the strings you already know — <8 left-aligns in 8 columns), and max(students, key=...) to pick the top student by a computed value. Everything structural — the class, the JSON parse, the comprehensions, the file read, the try/except — is exactly what you built step by step. That's the point: a real program is just the small pieces you already know, assembled with care. :::

Extend it (optional challenges)

Want to push further on your own machine? Each of these reuses only what you've learned:

  • Sort the report by average, highest first. (Hint: sorted(students, key=lambda s: s.average(), reverse=True).)
  • Write the report to a file instead of printing — open an output file in "w" mode and f.write each line.
  • Add a "class median" to the summary (sort the averages, take the middle one).
  • Handle a missing scores key gracefully with s.get("scores", []) so a malformed record doesn't crash the whole run.

Why it matters

This is the shape of real software: small, well-named functions; a class to model a thing; data parsed from JSON; comprehensions to transform it; files for input; error handling for the inevitable bad day — assembled into one program that does a real job. You didn't learn a new concept here; you proved you can combine the ones you have. That combining ability is what "I can program" actually means, and it's exactly the muscle every guide above this one will ask you to flex on bigger problems.

Where this leads: every project in the next guides is this same assembly — pieces you know, combined with care — just larger. See Where this leads next.

Checkpoint

Required checkpoint

Capstone project

Pass to unlock the Next button below

Next: Where this leads next →