ADR-001: Ruff-Only Golden Path¶
Status: Accepted Date: 2025-10-15 Deciders: Development team Context: Tooling standardization initiative
Context¶
Prior to October 2025, the project used multiple Python code quality tools:
- Black for code formatting
- isort for import sorting
- Flake8 for linting
- pylint for additional linting
- pycodestyle for PEP 8 style checking
- pydocstyle for docstring validation
This multi-tool approach created several problems:
- Configuration drift - Each tool had separate configuration files (
.flake8,.pylintrc,[tool.black],[tool.isort]) - Version conflicts - Tools had conflicting requirements and pinning was inconsistent
- Performance issues - Running 6 separate tools sequentially was slow (20-30 seconds per pre-commit)
- Conflicts between tools - Black and Flake8 disagreed on line length; isort and Black had import formatting conflicts
- Maintenance burden - Keeping 6 tools up-to-date and resolving conflicts
- Inconsistent behavior - Different results on local vs. CI due to version drift
The breaking point came when:
- Pre-commit hooks took >25 seconds to run
- CI builds failed due to tool version mismatches
- New contributors struggled with tool setup
- Conflicting auto-fixes required manual intervention
Decision¶
We will use Ruff as the single tool for all code formatting, linting, and import sorting.
Specifically:
- Replace Black with
ruff format- Drop-in compatible, 10-100x faster - Replace isort with
ruff check --select I- Compatible with Black profile - Replace Flake8/Pylint/pycodestyle/pydocstyle with
ruff check- Implements 800+ rules covering all previous tools - Pin Ruff version to
>=0.8.0inrequirements-dev.txtfor reproducibility - Centralize all configuration in
pyproject.tomlunder[tool.ruff] - Configure VS Code to use Ruff as default formatter (
charliermarsh.ruff) - Update pre-commit hooks to use only Ruff (remove Black, isort, Flake8 hooks)
Configuration standards:
- Line length: 88 characters (Black-compatible)
- Target: Python 3.12+
- Import sorting: "black" profile, first-party =
portfolio_management - Rule set: Comprehensive (E, F, W, C90, I, N, UP, B, A, C4, DTZ, T10, EM, EXE, ISC, ICN, PIE, PYI, PT, Q, RSE, RET, SIM, TID, ARG, PTH, PD, PGH, PL, TRY, FLY, PERF, LOG, RUF, D)
Consequences¶
Positive¶
- ✅ 10-100x faster execution - Pre-commit time reduced from 25s to \<3s
- ✅ Single source of truth - All configuration in
pyproject.toml - ✅ No tool conflicts - Ruff's formatter and linter are fully compatible
- ✅ Reproducible builds - Pinned version ensures consistency across environments
- ✅ Easier onboarding - New contributors install one tool instead of six
- ✅ Simpler maintenance - Update one tool instead of managing six separate versions
- ✅ Comprehensive coverage - 800+ rules cover all previous tools plus more
- ✅ Auto-fix capability -
ruff check --fixautomatically resolves many issues - ✅ Better VS Code integration - Native Ruff extension provides real-time feedback
Negative¶
- ⚠️ Tool lock-in - Dependent on Ruff's continued development and maintenance
- ⚠️ Migration effort - One-time cost to remove old tools and update configs
- ⚠️ Learning curve - Team needs to learn Ruff's rule codes and configuration
- ⚠️ Rule code differences - Ruff uses different codes than Flake8/Pylint (e.g., E501 vs. line-too-long)
Neutral¶
- 📋 Black compatibility - Ruff format is intentionally Black-compatible (no style changes)
- 📋 Incremental adoption - Can be adopted incrementally (e.g., format first, then linting)
- 📋 Active development - Ruff is under active development; new rules added regularly
Alternatives Considered¶
Option A: Keep Black + isort + Flake8¶
Description: Continue with the existing multi-tool stack, improve version pinning
Pros:
- No migration required
- Tools are mature and battle-tested
- Large community and extensive documentation
Cons:
- Slow execution (20-30 seconds)
- Configuration drift continues
- Tool conflicts unresolved
- Maintenance burden remains
Why rejected: Does not address root causes (speed, conflicts, complexity)
Option B: Use Black + Ruff (Hybrid)¶
Description: Keep Black for formatting, use Ruff only for linting
Pros:
- Smaller migration (only replace linters)
- Black is the "de facto standard" formatter
- Reduces risk of formatting changes
Cons:
- Still running two tools (slower than Ruff-only)
- Configuration split between two tools
- Potential conflicts remain (Black vs. Ruff linter)
- Doesn't maximize Ruff's speed benefits
Why rejected: Compromises most of the performance and simplicity gains
Option C: Use Pylint as Primary Linter¶
Description: Keep Black for formatting, use Pylint for comprehensive linting
Pros:
- Pylint has more advanced checks (e.g., design patterns)
- Mature tool with extensive documentation
Cons:
- Extremely slow (10-20x slower than Ruff)
- Complex configuration (pylintrc)
- Many false positives requiring extensive ignores
- Does not replace isort (still need 3 tools minimum)
Why rejected: Performance is unacceptable; complexity is higher than current state
Option D: Wait for PEP 723 / uv Integration¶
Description: Postpone decision until Python packaging ecosystem stabilizes
Pros:
- Future tooling might be even better
- Avoid premature standardization
Cons:
- No immediate solution to current problems
- Timeline is uncertain (PEP 723 is still draft)
- Pre-commit slowness impacts daily development
- Version conflicts block CI builds
Why rejected: Current problems are urgent; future tools can be adopted later if superior
Implementation Notes¶
The migration was completed in phases:
- Phase 1: Setup - Install Ruff, add to
requirements-dev.txt, configurepyproject.toml - Phase 2: Format - Run
ruff formaton entire codebase, verify no regressions - Phase 3: Lint - Enable Ruff linting rules incrementally, fix violations
- Phase 4: Pre-commit - Update
.pre-commit-config.yamlto use Ruff hooks only - Phase 5: Cleanup - Remove Black/isort/Flake8 from dependencies and configs
- Phase 6: Documentation - Create
RUFF_GOLDEN_PATH.md, updateCONTRIBUTING.md
Total migration time: ~8 hours across 2 sessions (October 15-28, 2025)