Project: A Sales Data Report
Time to combine what you've learned into one complete program. You'll build a small analytics tool that reads raw sales data from CSV, cleans and validates it, aggregates it several ways, and writes both a readable text report and a JSON summary — the kind of script people write at work every week.
The data
The input is a CSV export with some realistic problems: inconsistent capitalisation, stray spaces, a missing value and an invalid number.
date,region,product,units,unit_price 2026-09-01,North,Tea,12,3.50 2026-09-01,south,Cake,5,4.25 2026-09-02,North , Coffee,20,3.00 2026-09-02,East,Tea,,3.50 2026-09-03,South,Coffee,8,3.00 2026-09-03,East,Cake,seven,4.25 2026-09-04,north,Cake,6,4.25 2026-09-05,East,Coffee,15,3.00 2026-09-05,South,Tea,9,3.50
The complete program
Press Run. Then try the challenges at the end of the lesson.
How it's built
The program is split into small functions, each doing one job — a structure that makes it easy to test and extend:
| Function | Job | Concepts used |
|---|---|---|
Sale | A typed, immutable record with a computed revenue | Dataclasses, properties, type hints (lessons 33, 39, 40) |
parse_row | Clean and convert one row, or raise a clear error | String methods, conversion, custom exceptions, chaining (9, 7, 42) |
load_sales | Read every row, keeping valid ones and recording problems | csv.DictReader, enumerate, try/except (45, 17, 41) |
summarise | Aggregate totals by region and product | defaultdict, Counter, comprehensions, sorting (27, 25, 20) |
text_report | Format a human-readable table | f-string format specs (10) |
main | Wire it all together, write JSON | Files, json, __name__ guard (43, 45, 46) |
Notice the separation of concerns: parsing knows nothing about reports, and the summary is plain data that could equally be sent to a web page, a spreadsheet or an API.
Why bad rows are collected, not fatal
Real data is messy. Crashing on the first bad row would make the tool useless; silently ignoring bad rows would hide problems. Collecting them and reporting them at the end gives the user a correct report and tells them exactly what to fix.
Testing it
Because each function takes inputs and returns outputs, testing is straightforward:
import pytest from report import parse_row, load_sales, summarise, InvalidRow def test_parse_row_cleans_text(): sale = parse_row({"date": "2026-09-01", "region": " north ", "product": "tea", "units": "2", "unit_price": "3.5"}) assert sale.region == "North" and sale.revenue == 7.0 def test_parse_row_rejects_bad_units(): with pytest.raises(InvalidRow): parse_row({"date": "2026-09-01", "region": "N", "product": "T", "units": "seven", "unit_price": "1"}) def test_summary_totals(): sales, problems = load_sales("date,region,product,units,unit_price\n" "2026-09-01,N,Tea,2,3.0\n2026-09-02,S,Tea,1,3.0\n") assert summarise(sales)["total_revenue"] == 9.0 assert problems == []
Challenges
- Daily totals — add a "revenue by day" section, sorted by date (lessons 25, 49).
- Weekday analysis — which day of the week sells best? Use
sale.day.strftime("%A"). - Command-line options — on your computer, read the CSV path and an optional
--regionfilter withargparse(lesson 13). - Real files — save
sales.csvnext to the script and load it withpathlib(lesson 44). - Charts — plot revenue by region with Matplotlib, or load the CSV into pandas and compare (see the pandas course).
What next?
- Test yourself with the exercises and the quiz.
- Analyse data with NumPy, pandas and Data Science.
- Build web applications with Django.
- Explore AI and Generative AI.