Lab: API report
Fetch all todos from a public API, aggregate them by user, and write a plain-text summary report — end-to-end practice for the external integrations module.
- Fetch all items from a paginated public API endpoint
- Aggregate results by a field (userId) in Python
- Count completed vs incomplete items per user
- Write a formatted plain-text summary report
This lab pulls together everything from the external integrations module: making HTTP requests, reading response data, and writing useful output. You will work with the JSONPlaceholder API — a free, public fake API designed for testing. No authentication required.
The endpoint you will use: GET /todos returns 200 todo items across 10 users. Each
item looks like this:
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}Your goal: fetch all 200 items, count how many each user has and how many are completed, then write a formatted report.
Checkpoint 1: Fetch the data
JSONPlaceholder returns all 200 todos in one request — no pagination needed for this exercise. In a production script you would handle pagination (as covered in the previous lesson); here the focus is on the data processing pipeline.
You should see 200 todos and the structure of the first item. If the count is wrong or the structure has changed, stop here and inspect the raw response before continuing.
Checkpoint 2: Aggregate by user
Now process the list. For each user, count total todos and completed todos:
Each user has exactly 20 todos (200 total across 10 users). The completed counts will
vary. If you see unexpected user IDs or totals, check that the aggregation key matches
the field name exactly — "userId", not "user_id".
Checkpoint 3: Write the report
Format the aggregated data and write it to a file. The runner uses io.StringIO to
simulate writing without a real filesystem:
In a production script, replace the io.StringIO buffer with a real file path.
The rest of the logic is identical — that is why StringIO is useful for testing
the report format before committing to disk.
Putting it all together
Here is the complete script as it would look outside the browser, using requests and
writing a real file:
import requests
def fetch_todos():
response = requests.get("https://jsonplaceholder.typicode.com/todos")
response.raise_for_status()
return response.json()
def aggregate(todos):
stats = {}
for todo in todos:
uid = todo["userId"]
if uid not in stats:
stats[uid] = {"total": 0, "completed": 0}
stats[uid]["total"] += 1
if todo["completed"]:
stats[uid]["completed"] += 1
return stats
def write_report(todos, stats, path="todo_report.txt"):
with open(path, "w") as f:
f.write("TODO SUMMARY REPORT\n")
f.write("=" * 40 + "\n")
f.write(f"Total todos: {len(todos)}\n")
f.write(f"Users: {len(stats)}\n\n")
for uid in sorted(stats):
s = stats[uid]
incomplete = s["total"] - s["completed"]
pct = int(100 * s["completed"] / s["total"])
f.write(f"User {uid:2d}: {s['completed']:2d}/{s['total']} done ({pct}%), {incomplete} remaining\n")
print(f"Report written to {path}")
def main():
todos = fetch_todos()
stats = aggregate(todos)
write_report(todos, stats)
if __name__ == "__main__":
main()Each function has one job. fetch_todos handles the network call. aggregate handles
the counting. write_report handles the output. When something breaks — and it will —
this structure makes it easy to isolate which step failed.
Where to go next
The next module covers shell and processes — running external commands from Python, capturing their output, and chaining them into pipelines. This is the other major integration surface for automation scripts: the programs already installed on the system.
Pagination and rate limiting
Most APIs cap how many results they return per request and how many requests you can make per minute — learn to navigate both limits reliably.
Subprocess fundamentals
Python's subprocess module lets your scripts run external commands — understanding the difference between run() and Popen, and why shell=True is usually wrong, keeps your scripts safe and predictable.