Quality Checks: Ruff, Mypy, Pytest & Coverage
Up to now, Ruff has been running through the VS Code extension and pytest has been running on demand. Production-track projects need every check available from a terminal so contributors and CI can run the same commands. Four tools cover the ground: Ruff (linting and formatting), mypy (static types), pytest (tests), and coverage (test coverage reporting).
Adding the Dev Dependencies
Each tool installs into the project's dev group, not the runtime dependencies. They run on the developer's machine and in CI, but never inside the production container.
uv add --dev coverage mypy pytest ruff
pytest may already be in dev from earlier chapters; the second uv add --dev is a no-op for it. ruff was previously available via the VS Code extension only; pinning it as a project dev dependency means CI runs the same version contributors run.
Ruff: Lint and Format from the Command Line
Two commands:
uv run ruff check --no-cache .
uv run ruff format --check .
ruff check runs the lint rules configured in ruff.toml (the project has been carrying that file since the intro chapter). ruff format --check verifies code is already formatted; if a file needs changes, the command exits non-zero and prints the offending paths. Running ruff format . (no --check) rewrites the files in place.
Flags to note:
--fixonruff checkapplies safe auto-fixes (import sorting, unused-import removal, simple modernizations). Useful locally; CI should run the un-fixed form so a missed fix doesn't slip through.--no-cacheskips Ruff's on-disk cache. CI environments don't benefit from the cache, and skipping it avoids stale state in containerized builds.
Mypy: Static Type Checking
mypy reads the type annotations the codebase already has and checks they line up with how each value is used. Add it to pyproject.toml:
[tool.mypy]
python_version = "3.14"
warn_unused_ignores = true
warn_redundant_casts = true
disallow_incomplete_defs = true
no_implicit_optional = true
exclude = [
"^tests/",
"^alembic/versions/",
]
Run it:
uv run mypy .
Pytest with Coverage
pytest already runs on its own. Coverage data is what answers "which lines of the codebase did the tests actually exercise?" coverage runs the test suite and records every line that executed.
[tool.coverage.run]
branch = true
source = ["release_tracker"]
[tool.coverage.report]
show_missing = true
skip_covered = false
branch = true tracks both line and branch coverage, so a function whose if was always taken on the truthy side counts the falsy branch as missed. source = ["release_tracker"] scopes coverage to the application code; the test suite itself isn't measured.
Run the tests under coverage, then print the report:
uv run coverage run -m pytest
uv run coverage report
The report prints per-file totals, plus a list of line numbers that weren't covered. For a richer view, an HTML report drops into htmlcov/:
uv run coverage html
Open htmlcov/index.html and click into a file to see exactly which lines and branches the suite missed.
Coverage isn't a quality measurement
100% coverage means every line ran during tests, not that the tests are good. A test that calls a function and asserts nothing still bumps the coverage number. Treat the report as a tool for finding unintentional gaps (an entire route handler that no test touches), not as a target.
Recipe for Checks
Keeping the commands in the same order as CI runs them makes local-vs-CI mismatches easier to debug:
uv run ruff check --no-cache .
uv run ruff format --check .
uv run mypy .
uv run coverage run -m pytest
uv run coverage report
Larger projects might use a Makefile, a task runner, or pre-commit hooks.
Pre-commit hooks via prek
If you'd rather have these checks run automatically before each commit, prek (a fast Rust reimplementation of pre-commit) is recommended. Install it with:
uvx prek install
Configure the hooks in .pre-commit-config.yaml. The tooling is out of scope for this chapter; the pre-commit documentation covers the setup.