Today's outcomes
- Select and implement a bounded project contract.
- Test normal, invalid, empty, and round-trip cases.
- Present evidence of understanding and choose a focused next topic.
Before you start: Complete Day 29 and its practical challenge.
Understand the concept
Demonstrate a workflow, not a memorized list
Thirty days provide a structured introduction and practice, not automatic expertise. Your final task is to build a study-session reporter from a JSON document. It combines parsing, field validation, dates, patterns, collections, aggregation, files, and tests. You should be able to explain every accepted record and every exclusion.
The capstone accepts a list of records with exactly id, topic, date, and minutes. IDs have S followed by three ASCII digits. Topic is nonempty text after trimming. Date is the exact YYYY-MM-DD spelling of a valid calendar date. Minutes is an actual integer from 0 through 240. The first accepted occurrence of an ID wins; later valid occurrences are excluded. A rejected record does not reserve its ID.
Return accepted records and an audit containing one reason per rejected record, chosen in a documented validation order. Summarize count and total minutes by normalized topic, using casefold after trimming. Retain the original display topic in accepted records. This separates a comparison key from a display value, a distinction that appeared throughout the course.
Make the result accountable
Your report must reconcile input count with accepted plus rejected. Its total minutes must equal the sum of the accepted records. Topic totals must sum to the overall total. These invariants are more useful than a pretty report that silently drops rows or invents missing durations.
A mean needs a denominator. Count accepted sessions, including zero-minute sessions, and report None for the mean when no sessions are accepted. A missing duration is rejected, not counted as zero. The distinction changes both the numerator and denominator, so explain it in your write-up.
Date validation has two layers. A regex establishes the required spelling; date.fromisoformat establishes that the calendar date exists. Similarly, json.loads establishes syntax, while the record validator establishes your application shape. Reusing each tool at its own boundary is a central skill in this course.
Review deliberately before extending
Review the earlier material by reconstructing a small behavior from each group. You do not need to force inheritance into a project with no suitable hierarchy. Choose functions when they make the workflow clearer, and use classes when persistent state and behavior benefit from a shared model.
| Days | Review task | Evidence of understanding |
|---|---|---|
| 1–3 | Read values and produce a report | Explain types before and after conversion |
| 4–6 | Trace comparisons, branches, and loops | Predict accepted counts before running |
| 7–9 | Return results and choose sequence types | Test without depending on printed text |
| 10–12 | Use records, sets, and strings | Distinguish missing, zero, and duplicate |
| 13–15 | Format and transform collections | Preserve source data deliberately |
| 16–18 | Save, handle failures, and split modules | Reopen a saved result and import helpers |
| 19–22 | Explain object state and shared behavior | Justify functions versus a class |
| 23–24 | Parse dates and bounded patterns | Reject February 30 and near-miss IDs |
| 25–26 | Parse JSON and HTML | Separate syntax, extraction, and validation |
| 27–29 | Revisit your three projects | Show their acceptance tests passing |
Worked example
accepted = [
{"id": "S001", "topic": "Functions", "date": "2026-10-01", "minutes": 30},
{"id": "S002", "topic": "functions", "date": "2026-10-02", "minutes": 0},
{"id": "S003", "topic": "Files", "date": "2026-10-02", "minutes": 45},
]
by_topic = {}
for record in accepted:
key = record["topic"].casefold()
group = by_topic.setdefault(key, {"count": 0, "minutes": 0})
group["count"] += 1
group["minutes"] += record["minutes"]
total = sum(record["minutes"] for record in accepted)
assert total == 75
assert by_topic["functions"] == {"count": 2, "minutes": 30}
assert total / len(accepted) == 25
print(by_topic)
| Measure | Value | Definition |
|---|---|---|
| Accepted sessions | 3 | Includes the valid zero-minute session |
| Total minutes | 75 | Sum of accepted duration values |
| Mean minutes | 25 | Total divided by accepted count |
| Functions group | 2 sessions, 30 minutes | Casefolded comparison key |
The dates in this fixture are sample data, not claims about anyone's actual learning. Casefold establishes this project's comparison policy; it does not solve every language-specific naming or Unicode equivalence problem.
Guided practice
- Predict: add one duplicate ID, one invalid date, and one Boolean duration. Write accepted and rejected counts before running validation.
- Modify: sort a displayed topic report by total minutes descending, with topic name as a tie-breaker. Keep sorting separate from the aggregate values.
- Reconstruct: write the entire pipeline using only the contract and fixtures. Consult documentation when needed, then explain where each library function belongs.
Use a short design note before coding: input shape, validation order, duplicate policy, output shape, and empty-data behavior. It is much easier to test a stated rule than to infer a rule from the current implementation.
Daily challenge
Build the reporter, save its output as JSON, reload it, and verify the round trip. Demonstrate accepted, rejected, empty, and malformed-document cases. Include at least eight source records with three deliberate exclusions. Your write-up must name the fixture, show the audit, state the denominator, and list limitations.
| Criterion | Weight | Passing evidence |
|---|---|---|
| Correct pipeline | 40 | Parsing, validation, aggregation, and file round trip agree |
| Edge cases | 20 | Duplicate, invalid date, Boolean duration, empty list, malformed JSON |
| Readability | 15 | Clear function boundaries and meaningful names |
| Explanation | 15 | Policies and denominator are justified |
| Reproducibility | 10 | Saved fixture and runnable tests reproduce the report |
Aim for at least 85/100 in this practical rubric. Every required acceptance case must still work; a polished explanation cannot compensate for an incorrect total. The course's completion controls use a knowledge check plus your practical self-review, not remote execution or independent certification of your submitted code.
Reference solution
Compare after attempting the challenge
import json
import re
from datetime import date
from pathlib import Path
from tempfile import TemporaryDirectory
def validate_record(record):
if not isinstance(record, dict) or set(record) != {"id", "topic", "date", "minutes"}:
raise ValueError("expected exactly id, topic, date, minutes")
session_id = record["id"]
if not isinstance(session_id, str) or not re.fullmatch(r"S[0-9]{3}", session_id):
raise ValueError("invalid ID")
topic = record["topic"]
if not isinstance(topic, str) or not topic.strip():
raise ValueError("empty or nontext topic")
day = record["date"]
if not isinstance(day, str) or not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", day):
raise ValueError("date must use YYYY-MM-DD")
try:
date.fromisoformat(day)
except ValueError as error:
raise ValueError("invalid calendar date") from error
minutes = record["minutes"]
if type(minutes) is not int or not 0 <= minutes <= 240:
raise ValueError("minutes must be an integer from 0 to 240")
return {"id": session_id, "topic": topic.strip(), "date": day, "minutes": minutes}
def build_report(text):
records = json.loads(text)
if not isinstance(records, list):
raise ValueError("document root must be an array")
accepted, rejected, seen = [], [], set()
for row, record in enumerate(records, start=1):
try:
clean = validate_record(record)
if clean["id"] in seen:
raise ValueError("duplicate accepted ID")
except ValueError as error:
rejected.append({"row": row, "reason": str(error)})
else:
seen.add(clean["id"])
accepted.append(clean)
by_topic = {}
for record in accepted:
key = record["topic"].casefold()
group = by_topic.setdefault(key, {"count": 0, "minutes": 0})
group["count"] += 1
group["minutes"] += record["minutes"]
total = sum(record["minutes"] for record in accepted)
assert len(records) == len(accepted) + len(rejected)
assert total == sum(group["minutes"] for group in by_topic.values())
return {"input_count": len(records), "accepted": accepted, "rejected": rejected,
"total_minutes": total, "mean_minutes": total / len(accepted) if accepted else None,
"by_topic": by_topic}
fixture = [
{"id": "S001", "topic": "Functions", "date": "2026-10-01", "minutes": 30},
{"id": "S002", "topic": " functions ", "date": "2026-10-02", "minutes": 0},
{"id": "S003", "topic": "Files", "date": "2026-10-02", "minutes": 45},
{"id": "S004", "topic": "JSON", "date": "2026-10-03", "minutes": 25},
{"id": "S005", "topic": "JSON", "date": "2026-10-03", "minutes": 20},
{"id": "S001", "topic": "Duplicate", "date": "2026-10-03", "minutes": 10},
{"id": "S006", "topic": "Dates", "date": "2026-02-30", "minutes": 10},
{"id": "S007", "topic": "Types", "date": "2026-10-03", "minutes": True},
]
report = build_report(json.dumps(fixture))
assert len(report["accepted"]) == 5 and len(report["rejected"]) == 3
assert report["total_minutes"] == 120 and report["mean_minutes"] == 24
assert [item["row"] for item in report["rejected"]] == [6, 7, 8]
assert build_report("[]")["mean_minutes"] is None
for bad in ["{bad", "{}"]:
try:
build_report(bad)
except ValueError:
pass
else:
raise AssertionError("Invalid document accepted")
with TemporaryDirectory() as folder:
path = Path(folder) / "report.json"
path.write_text(json.dumps(report, indent=2, allow_nan=False), encoding="utf-8")
assert json.loads(path.read_text(encoding="utf-8")) == report
print("Capstone: 8 inputs = 5 accepted + 3 rejected; 120 minutes")
The report's mean is over accepted sessions, not input records or positive-duration sessions. JSONDecodeError inherits from ValueError, so the final document-level tests catch both malformed JSON and the explicit root-shape failure. Within the audit, only record-level validation failures become exclusions; a broken document fails before any report is built.
Debug and explain
If totals disagree, reconcile accepted records before inspecting formatting. If a rejected row blocks a later valid ID, check when seen is updated. If dates with invalid days pass, check that calendar parsing follows the regex. Explain the chosen validation order and why the report contains an exclusion reason rather than silently changing invalid values.
Review and next step
Finish by explaining the pipeline aloud without reading the reference. Revisit any day whose concept you cannot yet reconstruct. Then choose one extension with its own acceptance tests: a CSV export, date-range filter, atomic file save, or a small visual report. Continue with the Python and data foundations path, or practice execution in Python Quest. Keep your fixtures and tests; they are the evidence behind what your program claims to do.
Sources and verification
Original teaching examples checked with Python 3.12.14. Library behavior reviewed against these primary references on October 3, 2026. Day 26's examples also use Beautiful Soup 4.14.3.
Check your understanding
Five questions, 20 points each. Pass at 85% or above, which requires all five correct. Feedback explains each answer; revise the lesson and retry when needed.
Review your practical work
Check these statements after running your own challenge. This is your self-review; the website does not execute or assess your Python code.
Pass the knowledge check and finish the practical self-review to save completion.