Tool Decisions¶
A living reference of tool evaluation notes — what was chosen, what was skipped, and why. This complements the formal ADRs with the lighter-weight reasoning that comes up when comparing specific tools.
When to write here vs. an ADR: An ADR captures a significant architectural decision (e.g. "use pre-commit hooks" or "adopt a CI gate pattern"). This file captures the tool-level trade-off notes within those decisions (e.g. "typos over codespell because it's faster and broader").
Pre-commit Hooks¶
See ADR 008 for the architectural decision to use pre-commit. Notes below cover individual hook tool choices.
Chosen¶
| Tool | Category | Why chosen |
|---|---|---|
| Ruff | Lint + format | Single Rust binary that subsumes flake8, isort, pyupgrade, autopep8, and black. Orders of magnitude faster. Configured in one [tool.ruff] section. |
| typos | Spellcheck | Rust-based, faster and broader detection than codespell. Auto-fix support. Config in _typos.toml. |
| pip-audit | Vulnerability scan | PyPA-maintained, uses OSV + PyPI advisory DB. Successor to safety. |
| gitleaks | Secret detection | 150+ curated rules with entropy analysis. Single Go binary. Complements the fast regex no-secrets-patterns hook. |
| deptry | Dependency hygiene | No real alternative in Python. Compares pyproject.toml against actual imports to find unused, missing, and transitive deps. |
| actionlint | Workflow linting | Only mature GitHub Actions workflow linter. Catches expression errors, unknown runner labels, missing action inputs. |
| commitizen | Commit messages | Validates Conventional Commits format. Also provides cz commit for interactive authoring. |
| validate-pyproject | Config validation | Validates pyproject.toml against PEP 621 / packaging schemas. Catches schema errors before CI. |
| check-jsonschema | Config validation | SchemaStore-backed validation for GitHub workflows, actions, and Dependabot config. |
| no-commit-to-branch | Branch safety | Prevents accidental direct commits to main/master. Zero config, zero cost. |
| check-docstring-first | Code quality | Catches code placed before the module docstring — easy to miss in review. |
| prettier | Markdown tables | Manual-stage hook scoped to *.md. Fills the one formatting gap Ruff and markdownlint cannot: Markdown table alignment. Also available via task fmt:markdown. |
Skipped¶
| Tool | Category | Why skipped |
|---|---|---|
| pyupgrade | Code moderniser | Ruff's UP rule set includes all pyupgrade checks. Running both is redundant. |
| autopep8 | Formatter | Ruff's formatter and linter cover all autopep8 fixes. |
| black | Formatter | ruff-format is a drop-in replacement, faster, and configured alongside the linter in one tool. |
| isort | Import sorter | ruff check with isort rules (I) handles import sorting. Separate isort adds a dependency for duplicate functionality. |
| codespell (as a hook) | Spellcheck | typos is faster and has broader detection. codespell still runs in CI (spellcheck.yml) as a complementary safety net — the two overlap ~80% but each catches things the other misses. |
| safety | Vulnerability scan | pip-audit is the PyPA-maintained successor. safety's free tier scans an older, smaller database subset. |
| trufflehog | Secret detection | More complex setup, heavier runtime. gitleaks covers the same use case with a simpler single-binary approach. |
| Husky | Git hooks framework | Node.js-based. Adds a Node dependency to a Python project. pre-commit is Python-native and has a larger hook ecosystem. |
| markdownlint-cli2 (as active) | Markdown linting | Available as manual hook. Not active because it requires Node.js — heavier dependency footprint for optional benefit. |
| hadolint (as active) | Dockerfile linting | Available as manual hook. Requires Docker runtime. Also covered in CI by Trivy misconfig scanning. |
Notes¶
-
typos + codespell dual strategy: typos runs locally (fast, Rust-based, pre-commit stage). codespell runs in CI (
spellcheck.yml) with its own wordlist. They overlap ~80% but each catches things the other misses. Keeping both is intentional — not redundancy. -
typos exclude for generated files:
_typos.tomlexcludesCHANGELOG.md(generated by release-please) from spellchecking. The pre-commit hook also has a matchingexcludepattern as a belt-and-suspenders measure — typos may still check files passed explicitly by pre-commit even when the config says to skip them. -
deptry requires project venv: Unlike most hooks that run in pre-commit's isolated environment, deptry needs access to your installed packages to compare against imports. That's why it runs as a
local/systemhook viahatch run deptry .rather than as a remote-repo hook. -
pip-audit on pre-push, not pre-commit: Vulnerability scans hit a network API and can take several seconds. Running on every commit would be too slow. pre-push is the right trade-off — catches issues before code leaves your machine without slowing down every commit.
-
name-tests-test scoped to Python files: The
name-tests-testhook enforcestest_*.pynaming intests/andexperiments/. It must be restricted totypes: [python]to avoid false positives onREADME.mdand other non-Python files in those directories.
GitHub Actions Workflows¶
See ADR 003 for why workflows are in separate files, and ADR 024 for the CI gate pattern.
Chosen¶
| Workflow | Purpose | Why chosen |
|---|---|---|
| ci-gate.yml | Fan-in required check | Single gate status in branch protection instead of listing every workflow individually. Adding/removing workflows only requires editing one array in code — no Settings changes. |
| lint-format.yml | Ruff lint + format | Core quality gate. Ruff is the single linter/formatter (ADR 005). |
| test.yml | pytest matrix | Runs across Python 3.11–3.13. Core quality gate. |
| type-check.yml | mypy | Strict type checking (ADR 007). |
| coverage.yml | pytest-cov | Tracks coverage trends via Codecov (80% project target, 70% patch target). |
| security-audit.yml | pip-audit | Vulnerability scanning against OSV/PyPI DB. |
| bandit.yml | Bandit security lint | Path-filtered to src//scripts/ — only runs when Python source changes. |
| security-codeql.yml | GitHub CodeQL | Deep semantic analysis. Scheduled + PR-triggered. |
| dependency-review.yml | Dependency review | Flags new dependencies with known vulnerabilities on PRs. |
| container-build.yml | OCI image build | Builds the Containerfile on push/PR. |
| container-scan.yml | Trivy + Grype | Multi-scanner container vulnerability scanning (ADR 012). |
| spellcheck.yml | codespell in CI | Safety net for typos that the local typos hook might miss. |
| link-checker.yml | lychee | Validates URLs in docs. Path-filtered to *.md/*.html/docs/. |
| commit-lint.yml | Commit message validation | Ensures Conventional Commits on PRs (ADR 009). |
| pr-title.yml | PR title validation | Ensures PR titles follow conventional format. |
| release-please.yml | Automated releases | Changelog + version bumps (ADR 021). |
| scorecard.yml | OpenSSF Scorecard | Supply-chain security posture scoring. |
| sbom.yml | SBOM generation | Software Bill of Materials (ADR 013). |
| nightly-security.yml | Nightly full scan | Comprehensive security sweep on schedule. |
| stale.yml | Issue/PR cleanup | Auto-labels and closes stale issues/PRs. |
| labeler.yml | Auto-labeling | Labels PRs based on changed paths. |
| pre-commit-update.yml | Hook updates | Weekly PR to update pre-commit hook versions. |
| spellcheck-autofix.yml | Autofix typos | Weekly PR with codespell auto-corrections. |
| auto-merge-dependabot.yml | Auto-merge Dependabot | Auto-approves and squash-merges minor/patch Dependabot PRs once CI passes. |
| docs-build.yml | Docs CI gate | Runs mkdocs build --strict on every PR. Part of CI gate. |
| docs-deploy.yml | Docs deployment | Deploys to GitHub Pages on push to main. Path-filtered. |
Skipped / Not adopted¶
| Workflow idea | Why skipped |
|---|---|
| Mega CI workflow | Would consolidate all jobs into one file with needs:. Violates ADR 003 (separate files). Loses per-workflow triggers, permissions, path filters, and independent failure isolation. The ci-gate pattern solves the "single required check" problem without this trade-off. |
Notes¶
-
Path-filtered workflows and branch protection: Workflows with
paths:triggers (bandit, link-checker) can't be listed as required status checks in branch protection — they don't run on every PR, so GitHub would wait forever for a check that never appears. The ci-gate pattern handles this by only requiring always-run workflows in theREQUIRED_CHECKSlist. -
Scheduled workflows as safety nets: Most security and scanning workflows run on a weekly schedule in addition to PR triggers. This ensures that even if a path filter skips a check on a particular PR, the check still runs regularly on
main.
Python Tooling¶
Chosen¶
| Tool | Purpose | Why chosen |
|---|---|---|
| Hatch | Project manager + env manager | Manages virtualenvs, runs scripts, builds packages. Single tool replaces tox + pip-tools + build (ADR 016). |
| Hatchling | Build backend | PEP 517 build backend. Pairs with Hatch, auto-discovers src/ layout. |
| hatch-vcs | Version from git tags | Dynamic versioning from git tags. No manual version bumps. Fallback: 0.0.0+unknown. |
| pytest | Test framework | De facto standard. Rich plugin ecosystem (ADR 006). |
| pytest-cov | Coverage plugin | Integrates coverage.py with pytest. Branch coverage enabled, 80% minimum enforced. |
| mypy | Type checker | Strict mode. Best error messages for gradual typing (ADR 007). |
| Ruff | Linter + formatter | See pre-commit section above (ADR 005). |
| Bandit | Security linter | AST-based Python security analysis. Skips B101 (asserts) in config (ADR 018). |
Skipped¶
| Tool | Why skipped |
|---|---|
| tox | Hatch environments cover the same use case (matrix testing, isolated envs) with less configuration. |
| Poetry | Excellent tool but uses a non-standard [tool.poetry] config. Hatchling uses standard PEP 621 metadata. |
| setuptools | Works but requires more boilerplate. Hatchling auto-discovers src/ layout with zero config. |
| Flit | Simpler than setuptools, but lacks Hatchling's environment management and plugin ecosystem. No advantage when already using Hatch. |
| PDM | Full-featured alternative to Hatch. Less mature ecosystem and smaller community at time of evaluation. Hatch's PEP 621 compliance and env management won out. |
| Pyright | Valid alternative to mypy. mypy was chosen for broader community adoption and better error messages for gradual typing. Pyright is available via Pylance in VS Code for real-time feedback. |
| pip-tools | pip-compile is useful for pinning, but Hatch environments + pyproject.toml extras handle dependency management for this project's needs. See ADR 026 for detailed rationale. |
| nox | Python-native alternative to tox. More flexible than tox but adds a dependency Hatch already covers. |
Documentation Tooling¶
See ADR 020 for the architectural decision to use MkDocs.
Chosen¶
| Tool | Purpose | Why chosen |
|---|---|---|
| MkDocs | Static site generator | Python-native, simple config (mkdocs.yml), fast rebuild. Better fit for Python projects than Node-based alternatives. |
| mkdocs-material | Theme | Feature-rich Material Design theme. Built-in search, dark mode toggle, code copy, navigation tabs, content tabs. Active development. |
| mkdocstrings[python] | API docs from docstrings | Auto-generates API reference from Google-style docstrings. No manual duplication of function signatures. |
| pymdown-extensions | Markdown extensions | Adds tabbed content, details/summary, fenced code blocks with highlighting, inline code highlighting. |
Skipped¶
| Tool | Why skipped |
|---|---|
| Sphinx | More powerful for large API-heavy projects (cross-references, intersphinx). Steeper config, RST-centric (MyST bridges the gap but adds complexity). MkDocs is simpler for this project. |
| Docusaurus | React-based. Adds a full Node.js build pipeline to a Python project. Overkill for project documentation. |
| Jupyter Book | Sphinx-based, great for notebooks. Not needed here — no notebook-heavy documentation. |
| pdoc | Zero-config API docs. Too limited for project documentation beyond API reference — no custom pages, navigation, or theming. |
Notes¶
-
Strict mode in CI:
mkdocs build --strictruns on every PR viadocs-build.yml. This catches broken links, missing pages, and YAML errors before merge. -
GitHub Pages deployment:
docs-deploy.ymldeploys on push tomain, path-filtered todocs/,src/, andmkdocs.ymlchanges. -
MkDocs hooks have unit tests:
mkdocs-hooks/repo_links.py(~360 lines, 3 link pattern types, protected-region handling) is covered bytests/unit/test_repo_links.py(86 tests). Tests cover all helper functions, all three link patterns, the protect/restore system, and end-to-endon_page_markdownbehaviour.
Task Runner¶
See ADR 017 for the architectural decision.
Chosen¶
| Tool | Purpose | Why chosen |
|---|---|---|
| Task | Command runner | Go-based, single binary, Taskfile.yml config. Wraps hatch run commands for convenience. Cross-platform without shell compatibility issues. |
Skipped¶
| Tool | Why skipped |
|---|---|
| Make | Ubiquitous but tab-sensitive syntax, poor Windows support, file-based targets don't map well to "run this command" tasks. |
| Just | Rust-based, simpler than Make. Less adoption than Task, similar feature set. Task's YAML config is more readable for this project's needs. |
| nox | Python-native session runner. Good for test matrices but overlaps with Hatch's env management. Would add a dependency for duplicate functionality. |
| Invoke | Python-based, tasks.py config. Less declarative than Taskfile YAML. Adds a Python dependency when Task is language-agnostic. |
| npm scripts | Requires package.json in a Python project. Wrong ecosystem. |
Container Tooling¶
See ADR 019 and ADR 025 for architectural decisions.
Chosen¶
| Tool / Pattern | Purpose | Why chosen |
|---|---|---|
| Containerfile | Image definition | OCI-standard name (not Docker-specific Dockerfile). Works with both Podman and Docker (ADR 019). |
| Multi-stage build | Image size + security | Builder stage compiles wheel; runtime stage only has the installed package. No build tools, no source code in final image. |
| Digest pinning | Reproducibility | Base image pinned to SHA256 digest, not a mutable tag. Ensures identical builds regardless of when the image is pulled. |
| Non-root user | Runtime security | Creates app user (UID/GID 1000). Container never runs as root. |
| docker-compose.yml | Local orchestration | Lightweight scaffolding for docker compose up --build. Stubs for common services (DB, env, ports) — not production config. |
Skipped¶
| Tool / Pattern | Why skipped |
|---|---|
| Dockerfile name | Docker-specific convention. Containerfile is the OCI-standard name and works with both Docker and Podman. |
| Distroless base | Smaller attack surface but harder to debug (no shell). python:3.12-slim is a reasonable middle ground for a template project. |
| Alpine base | Smaller images but musl libc causes issues with some Python packages (numpy, pandas). Slim Debian avoids this class of problems. |
| Buildpacks | Fully managed build. Less control over the image contents. Overkill for a project with a simple pip install step. |
Release & Versioning¶
See ADR 021 for the release pipeline decision.
Chosen¶
| Tool | Purpose | Why chosen |
|---|---|---|
| release-please | Automated releases | Google-maintained. Generates changelogs from Conventional Commits with no manual intervention. Creates release PRs automatically. |
| hatch-vcs | Dynamic versioning | Version derived from git tags at build time. No manual version bumps. release-please creates the tags, hatch-vcs reads them. |
| commitizen | Commit message format | Validates Conventional Commits locally (commit-msg hook) and provides cz commit for interactive authoring. Feeds release-please. |
Skipped¶
| Tool | Why skipped |
|---|---|
| semantic-release | Node.js-based. Adds a Node dependency. release-please is GitHub-native (Action) and doesn't require a Node runtime. |
| bump2version | Manual version bumping. Redundant when hatch-vcs derives versions from git tags automatically. |
| setuptools-scm | Similar to hatch-vcs but tied to setuptools. hatch-vcs integrates natively with the Hatch/Hatchling build system already in use. |
| CalVer | Calendar-based versioning. SemVer is a better fit for a library/template where breaking changes need to be signaled in the version number. |
Notes¶
-
release-please
simpletype: Usesrelease-type: "simple"rather thanpythonbecause this is a template project, not a PyPI package. Theextra-filesconfig updates__init__.pywith the current version. -
Changelog sections: Only user-facing changes appear in
CHANGELOG.md(feat,fix,perf,revert). Docs, style, chore, refactor, test, build, and CI commits are hidden from the changelog but still visible in git history.
Coverage & Quality Metrics¶
Chosen¶
| Tool | Purpose | Why chosen |
|---|---|---|
| Codecov | Coverage tracking | Free for open source. GitHub integration with PR comments showing coverage diff. Supports branch coverage and per-flag reporting. |
| coverage.py | Coverage collection | De facto standard for Python coverage. Branch coverage enabled. Integrated via pytest-cov. |
Skipped¶
| Tool | Why skipped |
|---|---|
| Coveralls | Similar feature set to Codecov. Codecov has better GitHub Actions integration and more detailed PR comments. |
| SonarCloud | Full code quality platform (coverage + bugs + smells). Overkill — individual focused tools (Ruff, mypy, Codecov) cover each need. |
Notes¶
- Thresholds: Project target is 80% overall (1% threshold before failing).
Patch target is 70% on changed lines in PRs. Configured in
codecov.yml.
Dependency Automation¶
See ADR 010 for the Dependabot decision and ADR 032 for grouping strategy.
Chosen¶
| Tool / Pattern | Purpose | Why chosen |
|---|---|---|
| Dependabot | Dependency updates | GitHub-native. Monitors 3 ecosystems: github-actions, pip, docker. Weekly PRs with version bumps. |
| auto-merge-dependabot | Auto-merge safe updates | Auto-approves and squash-merges Dependabot PRs for minor/patch bumps once CI passes. Reduces maintenance noise. |
| pre-commit autoupdate | Hook version updates | pre-commit-update.yml runs weekly. Dependabot doesn't support pre-commit hooks natively, so this fills the gap. |
| Grouped updates | PR noise reduction | Minor and patch updates are grouped into single PRs per ecosystem. Reduces the number of Dependabot PRs to review. |
Skipped¶
| Tool | Why skipped |
|---|---|
| Renovate | More configurable than Dependabot (supports pre-commit, regex managers, monorepo grouping). Heavier setup — Dependabot is zero-config for GitHub repos. Revisit if Dependabot's limitations become painful. |
Notes¶
-
Three ecosystems:
github-actions(weekly, limit 5 PRs),pip(weekly, limit 10 PRs),docker(weekly, Containerfile base images). -
Pre-commit gap: Dependabot cannot update pre-commit hook versions. The
pre-commit-update.ymlworkflow runspre-commit autoupdateweekly and opens a PR with the changes.
Database Strategy¶
See ADR 027 for the architectural decision.
Chosen¶
| Tool / Pattern | Purpose | Why chosen |
|---|---|---|
| Raw SQL | Database access | No ORM. Plain SQL files in db/ with numbered migrations, seed data, and documented queries. Maximum transparency. |
| sqlite3 | Default database | stdlib module, zero setup. Good enough for templates and small apps. var/app.example.sqlite3 as example. |
Skipped¶
| Tool | Why skipped |
|---|---|
| SQLAlchemy | Powerful ORM but adds significant complexity. Raw SQL is more transparent for a template project. Easy to add later if needed. |
| Django ORM | Ties the project to Django. This is a standalone Python project, not a Django app. |
| Alembic | Migration tool for SQLAlchemy. Not needed without an ORM — numbered .sql files in db/migrations/ serve the same purpose with less. |
Label Management¶
See ADR 030 for the architectural decision.
Chosen¶
| Tool / Pattern | Purpose | Why chosen |
|---|---|---|
| Labels as code | Issue/PR labels | JSON definitions in labels/ applied via scripts/apply_labels.py. Reproducible, version-controlled, auditable. |
Notes¶
- Two label sets:
labels/baseline.json(core workflow labels) andlabels/extended.json(optional detailed labels). Apply withpython scripts/apply_labels.py.