By the end of this lesson
Run a complete CLI that validates a small task list before writing a summary.
Before you start
- Python 3.12 or a compatible newer Python 3 interpreter; no external packages.
- Complete lesson 20 in this route, or be comfortable with its examples.
Three words to know
- Contract
- The exact input and output behaviours a tool promises.
- Bounded read
- Reading at most a specified number of bytes plus one to detect excess.
- Fail closed
- Reject an invalid document rather than guessing a usable result.
1. Agree on the contract first
The input is one UTF-8 JSON array containing at most 1,000 objects. Each object must contain exactly title and status. title is nonblank text of at most 100 Python characters; status is exactly open or closed. Empty input lists are allowed. File size is at most 65,536 bytes, including whitespace. Duplicate object keys and non-standard NaN or Infinity constants are rejected. The code does not normalise or rewrite titles.
2. Save a known input
Create a new file named tasks.json in your practice folder using your editor. Paste the JSON below. The JSON block is file content, not a terminal command. The download links also provide this fixture and the source.
[{"title":"Read","status":"open"},{"title":"Test","status":"closed"},{"title":"Review","status":"open"}]
3. Read the complete implementation
Save this source as task_report.py in the same folder. summarise validates decoded values and returns data. read_records performs the bounded read and strict UTF-8 decode. main handles the CLI boundary. No summary is printed until every record has passed validation. The script only reads the named file and writes stdout or stderr; it does not modify the input.
# SPDX-License-Identifier: MIT
"""Learning project: bounded local task-status reporting, not a general JSON validator."""
import argparse
import json
import sys
from pathlib import Path
MAX_BYTES = 65536
MAX_RECORDS = 1000
def reject_constant(value):
raise ValueError("non-standard JSON constant")
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate JSON key")
result[key] = value
return result
def summarise(records):
if not isinstance(records, list) or len(records) > MAX_RECORDS:
raise ValueError("expected at most 1000 records")
counts = {"closed": 0, "open": 0}
for record in records:
if not isinstance(record, dict) or set(record) != {"title", "status"}:
raise ValueError("expected title and status only")
title, status = record["title"], record["status"]
if not isinstance(title, str) or not title.strip() or len(title) > 100:
raise ValueError("expected a nonblank title of at most 100 characters")
if not isinstance(status, str) or status not in counts:
raise ValueError("expected open or closed status")
counts[status] += 1
return {"records": len(records), "by_status": counts}
def read_records(path):
with Path(path).open("rb") as handle:
raw = handle.read(MAX_BYTES + 1)
if len(raw) > MAX_BYTES:
raise ValueError("input exceeds 65536 bytes")
return json.loads(raw.decode("utf-8"), parse_constant=reject_constant,
object_pairs_hook=unique_object)
def main(argv=None):
parser = argparse.ArgumentParser(description="Summarise a small local task list.")
parser.add_argument("input", help="UTF-8 JSON file, at most 65536 bytes")
args = parser.parse_args(argv)
try:
result = summarise(read_records(args.input))
except (OSError, UnicodeError, ValueError, RecursionError):
print("error: invalid or unreadable task file", file=sys.stderr)
return 2
print(json.dumps(result, sort_keys=True, allow_nan=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
What you should see · example output
{"by_status": {"closed": 1, "open": 2}, "records": 3}
4. Run and explain the result
python3 task_report.py tasks.json
What you should see · example output
{"by_status": {"closed": 1, "open": 2}, "records": 3}Windows: use py -3 task_report.py tasks.json. You should see three records, one closed and two open. Output includes both status counters even when one is zero. A bad document produces one generic stderr message, exit status 2 and no JSON summary. A missing CLI argument is handled separately by argparse. The error message deliberately avoids echoing potentially private file contents.
YOUR TURN
A small check before you continue
Change one status to pending and run again. Should the two valid records still produce a partial summary?
Show a suggested answer
No. The entire document is rejected: stderr reports error: invalid or unreadable task file, the status is 2 and stdout is empty. Restore open or closed before rerunning.
Sources and scope
Documentation and examples reviewed 2026-10-18. Provider screens, package versions and output wording can differ. Examples show expected outcomes; they are not a claim that OpenViST connected to your server or verified your setup.
