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.

TXT sales.csv
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.

Try it yourself
Console output

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:

FunctionJobConcepts used
SaleA typed, immutable record with a computed revenueDataclasses, properties, type hints (lessons 33, 39, 40)
parse_rowClean and convert one row, or raise a clear errorString methods, conversion, custom exceptions, chaining (9, 7, 42)
load_salesRead every row, keeping valid ones and recording problemscsv.DictReader, enumerate, try/except (45, 17, 41)
summariseAggregate totals by region and productdefaultdict, Counter, comprehensions, sorting (27, 25, 20)
text_reportFormat a human-readable tablef-string format specs (10)
mainWire it all together, write JSONFiles, 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:

PY test_report.py
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

  1. Daily totals — add a "revenue by day" section, sorted by date (lessons 25, 49).
  2. Weekday analysis — which day of the week sells best? Use sale.day.strftime("%A").
  3. Command-line options — on your computer, read the CSV path and an optional --region filter with argparse (lesson 13).
  4. Real files — save sales.csv next to the script and load it with pathlib (lesson 44).
  5. Charts — plot revenue by region with Matplotlib, or load the CSV into pandas and compare (see the pandas course).

What next?