A two-file Flask app that demonstrates flask-coverage and shows realistic coverage numbers as you exercise it.
| File | Role |
|---|---|
demo.py |
Tiny bootstrap. Calls start_early() before importing the app, so coverage is active for everything below. |
demo_app.py |
The actual app — three routes, an app factory, FlaskCoverage(app). Fully measurable from line 1. |
The split exists because coverage.py can only trace lines that execute after Coverage.start() runs. If route definitions and the factory live in the same file as the bootstrap, those module-level lines are missed even though the tool is working correctly. Putting them in a separate module — imported only after start_early() — sidesteps the problem and gives an honest 90%+ number when all routes are exercised. (For real projects, the same pattern works: wsgi.py does the bootstrap, myapp/ is the application package.)
| Route | Purpose |
|---|---|
GET / |
Always-hit route — covered after the first request. |
GET /maybe |
Two branches selected by the ?fail=1 query string — exercise the gap. |
GET /never-called |
Wired up but not called by the walkthrough — its body shows as missing in the report. |
From the repository root:
uv sync
uv run flask --app examples/demo run --debug--debug is what authorises the /debug/coverage blueprint without needing a password.
In a second terminal:
# 1. Open the dashboard. demo_app.py should already be at ~72% — its
# module-level setup ran during import (with coverage already active).
open http://127.0.0.1:5000/debug/coverage/
# 2. Exercise / and one branch of /maybe.
curl http://127.0.0.1:5000/
curl http://127.0.0.1:5000/maybe
# 3. Refresh the dashboard. Coverage on demo_app.py rises to ~88%; the
# fail-branch of /maybe and never_called's body are still missing.
# 4. Hit the other branch.
curl 'http://127.0.0.1:5000/maybe?fail=1'
# 5. Refresh again. demo_app.py reaches ~94%. Only never_called's body
# remains uncovered — exactly as documented.Click html report in the dashboard nav (or GET /debug/coverage/html/) to see the line-by-line source view with covered / missing lines highlighted. For programmatic consumption (e.g. CI gating), GET /debug/coverage/files returns the same data as JSON.
The bootstrap module shows poor coverage in the report — that's expected. Its top three lines (docstring, from __future__ import annotations, the import of start_early) necessarily run before start_early() is called, so they can't be traced. That's the price of starting coverage in user-space code rather than via COVERAGE_PROCESS_START (see below).
Production-style with HTTP basic auth (no --debug):
FLASK_COVERAGE_PASSWORD=s3cret \
uv run flask --app examples/demo runHit the dashboard with credentials:
curl -u admin:s3cret http://127.0.0.1:5000/debug/coverage/reportDisable without redeploying:
FLASK_COVERAGE_DISABLED=1 \
uv run flask --app examples/demo run --debugFlaskCoverage(app) and start_early() both become no-ops — no tracer, no blueprint, no auth check. Useful when an operator needs to pull tracing fast.
Start coverage from outside the process — the tidiest option for real deployments, because then nothing in user code is missed:
COVERAGE_PROCESS_START=$(pwd)/pyproject.toml \
uv run flask --app examples/demo run --debugstart_early() detects the already-running tracer and is a no-op; FlaskCoverage(app) adopts it.
The demo writes a .coverage file in your current directory:
rm -f .coverage .coverage.*