Skip to content

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:

  1. Configuration drift - Each tool had separate configuration files (.flake8, .pylintrc, [tool.black], [tool.isort])
  2. Version conflicts - Tools had conflicting requirements and pinning was inconsistent
  3. Performance issues - Running 6 separate tools sequentially was slow (20-30 seconds per pre-commit)
  4. Conflicts between tools - Black and Flake8 disagreed on line length; isort and Black had import formatting conflicts
  5. Maintenance burden - Keeping 6 tools up-to-date and resolving conflicts
  6. 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:

  1. Replace Black with ruff format - Drop-in compatible, 10-100x faster
  2. Replace isort with ruff check --select I - Compatible with Black profile
  3. Replace Flake8/Pylint/pycodestyle/pydocstyle with ruff check - Implements 800+ rules covering all previous tools
  4. Pin Ruff version to >=0.8.0 in requirements-dev.txt for reproducibility
  5. Centralize all configuration in pyproject.toml under [tool.ruff]
  6. Configure VS Code to use Ruff as default formatter (charliermarsh.ruff)
  7. 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 --fix automatically 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:

  1. Phase 1: Setup - Install Ruff, add to requirements-dev.txt, configure pyproject.toml
  2. Phase 2: Format - Run ruff format on entire codebase, verify no regressions
  3. Phase 3: Lint - Enable Ruff linting rules incrementally, fix violations
  4. Phase 4: Pre-commit - Update .pre-commit-config.yaml to use Ruff hooks only
  5. Phase 5: Cleanup - Remove Black/isort/Flake8 from dependencies and configs
  6. Phase 6: Documentation - Create RUFF_GOLDEN_PATH.md, update CONTRIBUTING.md

Total migration time: ~8 hours across 2 sessions (October 15-28, 2025)

References