Review and Stretch Goals

8 min read

You've built api-monitor from scratch — read a config, hit endpoints, validate against a contract, render two reports, exit cleanly. That's a real piece of QA tooling. This final lesson is for the part that's easy to skip but most valuable: pausing to take stock of what you've learnt, looking honestly at what you'd improve, and choosing the next move. We'll cover a self-assessment checklist, reflection questions worth asking out loud, five stretch goals to pick from for portfolio polish, and a map of where Python skills go from here.

Self-assessment — concept by concept

Run through this checklist by reading your api-monitor code. For each item, decide: I used it confidently / I used it but had to look it up / I haven't used it. Anything in the second category is fair game for revision; anything in the third is a candidate for the next project, not a gap to panic about.

Chapter 1 — Foundations. Did you set up the project in a venv? Activated before installing? Did you remember python -m venv venv?

Chapter 2 — Control flow & functions. Are your if chains using elif? Did you reach for a list comprehension to filter expected_fields? Did your functions have type hints on parameters and return values?

Chapter 3 — Data structures. Did you use a dict for the parsed config, a list for results, and a list comprehension for filtering? Did you reach for set() anywhere — for instance, to compare expected vs received fields?

Chapter 4 — Files & APIs. Did you use with open(...) for the JSON reads and writes, with encoding="utf-8"? Did you pass timeout= to every requests.get? Did you check response.status_code before response.json()?

Chapter 5 — Object-oriented Python. Are Endpoint and CheckResult dataclasses? Did field(default_factory=list) save you from the mutable-default trap? Did you reach for @property or methods on a dataclass?

Chapter 6 — Errors and modules. Is ConfigError a custom class? Does it inherit from Exception? Did you use raise X from e to chain the original JSONDecodeError? Are imports in src/ consistent (relative or absolute, not mixed)?

Chapter 7 — pytest. (Stretch goal — even if you skipped it now, walk through what tests you'd write.) Could you mock requests.get to test check_endpoint without hitting a real server? Would each test cover one path: success, wrong status, slow, missing field, timeout?

You don't need every box ticked. The goal is to know which lessons paid off most and which deserve a re-read.

Reflection questions

Ask yourself three questions out loud — or better, write the answers in your README.

1. What was the hardest part? For most learners it's not the syntax. It's something architectural: deciding what belongs in monitor.py vs reporter.py, or where to draw the line between a config error and a check failure, or how to handle one endpoint failing without crashing the rest. The decisions you got stuck on are the ones worth remembering.

2. What would you change with hindsight? Read your code today as if a new teammate had written it. What's confusing? What's clever for clever's sake? Which file does too much? Which would you split? Refactor one thing — even a small one — to demonstrate you can.

3. What's the next thing this script should do? Production health monitors don't stay simple — they grow retries, alerting, history. Pick the one feature you'd add first if your team adopted this script tomorrow. That feature is your stretch goal.

These questions are the things a senior engineer asks in a code review. Practising them on your own work is how you learn to spot them in others'.

Five stretch goals — pick the ones that interest you

Each one teaches something distinct. Don't try to do all five — pick one or two and do them well.

1. Retry logic on transient errors

Wrap the request in a small retry loop. Retry on requests.Timeout and on 5xx responses; don't retry on 4xx (those are real errors in the request). Add time.sleep(0.5 * attempt) between tries.

def fetch_with_retry(url: str, max_time_ms: int, retries: int = 2):
    for attempt in range(retries + 1):
        try:
            return requests.get(url, timeout=max(max_time_ms / 1000 * 2, 5))
        except (requests.Timeout, requests.ConnectionError):
            if attempt == retries:
                raise
            time.sleep(0.5 * (attempt + 1))

Skill: chapter 6's try/except plus a real-world back-off pattern.

2. Email notification on failure

Use Python's standard library smtplib to send an email summary when one or more checks failed. No third-party packages needed — every Python install has it.

The body of the email is just the same string print_console builds (refactor it to a function that returns a string, then use it for both stdout and the email).

Skill: standard-library exploration; learning to refactor for reuse.

3. Historical tracking with CSV

Append every run's totals to output/history.csv — timestamp, total, passed, failed, average response time. After ten runs you have a trend. After a hundred you can spot drift before it becomes a regression.

import csv
from datetime import datetime
 
def append_history(results, path="output/history.csv"):
    new_file = not Path(path).exists()
    with open(path, "a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=[
            "timestamp", "total", "passed", "failed", "avg_time_ms"
        ])
        if new_file:
            writer.writeheader()
        writer.writerow({
            "timestamp": datetime.now().isoformat(timespec="seconds"),
            "total": len(results),
            "passed": sum(1 for r in results if r.passed),
            "failed": sum(1 for r in results if not r.passed),
            "avg_time_ms": round(sum(r.actual_time_ms or 0 for r in results) / len(results)),
        })

Skill: chapter 4's CSV append mode + idempotent file initialisation.

4. Pytest tests with mocked responses

Write a test suite for monitor.check_endpoint without hitting a real server. Mock requests.get so each test controls exactly what comes back:

from unittest.mock import patch, MagicMock
from src.monitor import check_endpoint
from src.models import Endpoint
 
def test_passes_on_2xx_under_threshold():
    fake = MagicMock(status_code=200, elapsed=MagicMock(total_seconds=lambda: 0.05))
    fake.json.return_value = {}
    with patch("src.monitor.requests.get", return_value=fake):
        result = check_endpoint(Endpoint(name="t", url="http://x", expected_status=200, max_time_ms=1000))
    assert result.passed

Add tests for: wrong status, slow response, missing field, timeout, connection error. Five tests, five distinct paths. The one test suite proves the whole script works without the network.

Skill: chapter 7's pytest plus the unittest.mock.patch helper. This is exactly how production Python tests are written.

5. Parallel checking with concurrent.futures

Sequential checks are fine for ten endpoints. For thirty, you wait thirty seconds. Run them in parallel:

from concurrent.futures import ThreadPoolExecutor
 
def check_all(endpoints):
    with ThreadPoolExecutor(max_workers=10) as pool:
        return list(pool.map(check_endpoint, endpoints))

Three lines and your script finishes in roughly the time of the slowest single check. ThreadPoolExecutor is the right tool here because each request spends almost all its time waiting on the network — exactly what threads are good at.

Skill: concurrency concepts that scale beyond this script.

Where to go next

You're now a real Python QA engineer in the small. The natural next steps:

  • Playwright with Python — apply the same skills to UI automation. The requests-style API maps cleanly onto Playwright's page.goto, page.fill, page.click. Page Object Model from chapter 5 plugs in directly.
  • API Testing Masterclass — go deeper into HTTP, contracts, schema validation, fuzzing, contract testing with tools like Pact.
  • CI/CD for QA Engineers — wire api-monitor into a real pipeline. GitHub Actions, GitLab, Jenkins. The script you built is already CI-ready; learn to deploy and schedule it.
  • Performance testing with Python (Locust) — once you can hit one endpoint, hitting a thousand a second is a natural extension.
  • Test data engineering with pandas — the same dicts and lists you handled here, scaled to spreadsheets and dataframes.

A map of where Python takes you

Python for QA (you are here)
  • – Playwright (Python)
  • – Selenium (Python bindings)
  • – API Testing Masterclass
  • – Schema validation (jsonschema)
  • – Contract testing (Pact)
  • – CI/CD for QA
  • – Docker for tests
  • – Test reporting at scale
  • Locust for load tests –
  • pandas for test telemetry –
  • pytest-benchmark –

Four directions, all reachable from where you are now. None require relearning Python — they're applications of what you've already built. Pick whichever your team needs most or whichever excites you most. Either is a good answer.

Final words

You've come a long way: from "what is Python" to a working monitor that pulls together file I/O, HTTP, dataclasses, error handling, and modules. The same handful of patterns you used here — read input, do work, validate, report — are 80% of every QA script you'll ever write in Python. The other 20% is library knowledge: which client, which testing framework, which reporting tool. Those names will change every few years; the patterns won't.

Two habits to take with you:

  1. Default to a venv. Every project, day one. Five seconds of setup buys you total isolation.
  2. Write the test, then the code. Even a one-test scaffold steers your design toward something testable. The capstone's stretch goal #4 is the cleanest place to start.

Now go build something. Not another tutorial — something you need. A script that renames test screenshots into a coherent folder structure. A tool that compares two CI runs and tells you which tests changed. A wrapper around your team's API that makes data-driven tests trivially easy. The skills you've built in this course are the ones that take an idea from your notes to a tool your colleagues thank you for.

You're done. Welcome to the job.

// tip to track lessons you complete and pick up where you left off across devices.