Parse messy text records, skip bad lines, group by key and report summary statistics.
Why it matters
This lesson pulls the whole course together. Turning raw text records into a clear summary is a task you will meet in almost every technical job: an engineer summarising sensor logs, a lab technician checking instrument output, a teacher totalling grades, a data analyst cleaning an export. The same five steps appear every time: read, parse, validate, group, report.
The problem
A building's sensors write one line per reading:
# date,sensor,value
2026-10-01,temp-a,21.5
2026-10-01,temp-b,19.0
2026-10-02,temp-a,22.5
2026-10-02,temp-b,n/a
2026-10-03,temp-a,20.0
2026-10-03,humid,55
garbage line
Real logs are messy. This one has a comment line, a blank line, a reading of n/a and a broken line. We want a report like:
humid: n=1 min=55.0 max=55.0 mean=55.0
temp-a: n=3 min=20.0 max=22.5 mean=21.3
temp-b: n=1 min=19.0 max=19.0 mean=19.0
The same design works for grades: replace "sensor" with "student" or "assignment" and "value" with "score".
Step 1: parse one line
Start with the smallest piece. A function that takes one line and returns a clean record, or None if the line is not usable:
def parse_line(line):
line = line.strip()
if not line or line.startswith("#"):
return None
parts = line.split(",")
if len(parts) != 3:
return None
day, sensor, text = parts
try:
value = float(text)
except ValueError:
return None
return (day, sensor, value)
Every way a line can be bad is handled explicitly: blank, comment, wrong number of fields, value that is not a number. Returning None for "skip this" keeps the decision in one place.
Test it before moving on:
assert parse_line("2026-10-01,temp-a,21.5") == ("2026-10-01", "temp-a", 21.5)
assert parse_line("# header") is None
assert parse_line("2026-10-02,temp-b,n/a") is None
assert parse_line("garbage line") is None
Step 2: load every line
def load_records(text):
records = []
for line in text.splitlines():
record = parse_line(line)
if record is not None:
records.append(record)
return records
text.splitlines() splits a block of text into lines. If the data were in a file, you would loop over the file object instead, exactly as in the files lesson. Keeping load_records working on a string makes it easy to test, and a one-line wrapper can read a file into that string.
Step 3: group by sensor
This is the dictionary-of-lists pattern from the dictionaries lesson:
def group_by_sensor(records):
groups = {}
for day, sensor, value in records:
groups.setdefault(sensor, []).append(value)
return groups
groups.setdefault(sensor, []) returns the existing list for that sensor, or creates an empty one first. It is a compact version of the if sensor not in groups: check.
Step 4: summarise each group
import statistics
def summarise(records):
summary = {}
for sensor, values in group_by_sensor(records).items():
summary[sensor] = {
"count": len(values),
"min": min(values),
"max": max(values),
"mean": statistics.mean(values),
}
return summary
The result is a dictionary of dictionaries, easy to test and easy to turn into any output format.
Step 5: format the report
def format_report(summary):
lines = []
for sensor in sorted(summary):
s = summary[sensor]
lines.append(
f"{sensor}: n={s['count']} min={s['min']:.1f} "
f"max={s['max']:.1f} mean={s['mean']:.1f}"
)
return "\n".join(lines)
Sorting the sensor names makes the output predictable, which matters for testing and for readers comparing two reports. "\n".join(lines) glues the lines together with newlines between them.
Putting it together
report = format_report(summarise(load_records(LOG_TEXT)))
print(report)
Each function does one job, and the final line reads almost like the plan: load, summarise, format. When something is wrong, you can test each stage separately to see where the data stops looking right.
Extending it
Once the core works, good next steps are:
- Count what was skipped. Report "3 lines skipped" so bad data does not vanish silently.
- Flag out-of-range values. Reuse your
reading_statusfunction from the conditions lesson. - Write the report to a file with
with open("report.txt", "w"). - Group by day as well as sensor, using a tuple
(day, sensor)as the dictionary key. - Grades version: a CSV of
student,assignment,score, with a letter grade per student from their mean.
Common mistakes
- Writing everything in one big loop. It works for the first version, then becomes impossible to change. Split it into functions.
- Silently dropping data. Skipping bad lines is right, but count them so you know how much was lost.
- Testing only the happy path. Include a blank line, a comment, a non-number and a short line in your test data.
- Unpredictable output order. Sort keys before printing if a person or a test will compare the output.
Quick recap
- Read, parse, validate, group, report: the shape of most data-summary programs.
- Parse one line in a function that returns a record or
None. - Group with a dictionary of lists, then summarise each group.
- Format the report last, in a predictable order.
- Test each function on its own with small, deliberately messy inputs.
Try it
The Do tab gives you the log above and the five function names. Implement them so the checks pass, then print the report.