Use Ruff for Linting and Formatting⚓︎
- Status: accepted
- Date: 2023-03-15 (backfilled 2024-11-20)
- Deciders: KyleKing
- Informed: calcipy users
Context and Problem Statement⚓︎
calcipy bundles linting and code formatting tools to enforce code quality standards across projects. Historically, this required running multiple tools:
- flake8 for linting
- pylint for additional linting
- black for code formatting
- isort for import sorting
- pyupgrade for syntax modernization
- autoflake for removing unused imports
Each tool had its own configuration, ran independently, and required separate dependencies. The combined execution time was significant (30-60 seconds for medium projects), and configuration conflicts between tools were common.
How can we provide comprehensive linting and formatting while minimizing dependencies, execution time, and configuration complexity?
Decision Drivers⚓︎
- Must provide comprehensive linting coverage (equivalent to flake8 + pylint + others)
- Must support auto-fixing for common issues
- Must be fast (developers won’t use slow tools)
- Must minimize dependency count (calcipy aims to be lightweight)
- Must have good configuration story (ideally in pyproject.toml)
- Must be actively maintained with regular updates
- Prefer Rust-based tools for performance (already proven with ripgrep, fd, etc.)
- Must support all rules needed for calcipy’s strict quality standards
Considered Options⚓︎
- Ruff - Fast Python linter and formatter written in Rust
- Flake8 + Black + isort - Traditional combination of tools
- Pylint + Black + isort - More comprehensive linting
- Flake8 + Pylint + Black + isort - Maximum coverage
- Pylama - Meta-linter that wraps multiple tools
Decision Outcome⚓︎
Chosen option: “Ruff”, because it consolidates multiple tools into one fast executable while providing equivalent or better linting coverage. The performance improvement (10-100x faster) and simplified configuration justify the switch from the traditional toolchain.
Ruff provides:
- All flake8 plugins (100+) in a single tool
- Import sorting (isort compatibility)
- Auto-fixing for hundreds of rules
- Pyupgrade and autoflake functionality built-in
- Extremely fast execution (written in Rust)
- Single configuration in pyproject.toml
- Active development and frequent releases
- Growing adoption in the Python community
We later removed pylint entirely (v4.1.0) after validating that ruff’s coverage was sufficient.
Consequences⚓︎
- Good, because linting is 10-100x faster than previous toolchain
- Good, because single tool simplifies dependency management
- Good, because unified configuration in pyproject.toml
- Good, because auto-fixing is comprehensive and reliable
- Good, because active development with frequent rule additions
- Good, because growing community adoption reduces risk
- Good, because can remove black, isort, flake8, pylint, pyupgrade, autoflake dependencies
- Bad, because requires Rust toolchain for building from source (rare case)
- Bad, because some pylint rules not yet available (gap closing)
- Bad, because newer tool (less battle-tested than flake8/pylint)
- Neutral, because different rule codes require configuration migration
- Neutral, because some unique pylint patterns need refactoring
Pros and Cons of the Options⚓︎
Ruff⚓︎
- Good, because extremely fast (10-100x faster than flake8)
- Good, because consolidates 8+ tools into one
- Good, because comprehensive rule coverage (1000+ rules)
- Good, because excellent auto-fixing support
- Good, because single configuration location
- Good, because active development (weekly releases)
- Good, because reducing to single tool simplifies maintenance
- Bad, because newer tool (less mature than flake8/pylint)
- Bad, because some niche pylint rules not available
- Bad, because requires configuration migration
Flake8 + Black + isort⚓︎
- Good, because mature, well-tested tools
- Good, because widespread adoption
- Good, because stable APIs and configurations
- Bad, because slow execution (multiple processes)
- Bad, because multiple configurations to maintain
- Bad, because dependency on 10+ packages (flake8 plugins)
- Bad, because configuration conflicts between tools
- Bad, because limited auto-fixing
Pylint + Black + isort⚓︎
- Good, because comprehensive linting (pylint finds complex issues)
- Good, because mature tools
- Bad, because very slow (pylint is slowest major linter)
- Bad, because pylint produces many false positives
- Bad, because configuration is complex
- Bad, because multiple tools to run and configure
- Bad, because poor auto-fixing support
Flake8 + Pylint + Black + isort⚓︎
- Good, because maximum linting coverage
- Good, because catches most issues
- Bad, because extremely slow (60+ seconds for medium projects)
- Bad, because highest dependency count
- Bad, because most complex configuration
- Bad, because overlapping/conflicting rules
- Bad, because diminishing returns for marginal coverage increase
Pylama⚓︎
- Good, because consolidates multiple linters
- Good, because single configuration
- Bad, because just a wrapper (doesn’t improve speed)
- Bad, because adds abstraction layer
- Bad, because limited compared to individual tools
- Bad, because less active development
Validation⚓︎
This decision was validated through:
- Migration completed in v3.x without major issues
- Linting time reduced from ~45 seconds to ~2 seconds
- Removal of 8+ tool dependencies
- Successful use in production for 2+ years
- Pylint removed in v4.1.0 without negative impact
- Community adoption of ruff has grown significantly
We would revisit this decision if:
- Ruff development slows or project becomes unmaintained
- Critical pylint functionality is needed that ruff can’t provide
- Performance degrades significantly
- Configuration becomes too complex
More Information⚓︎
- Ruff documentation: https://docs.astral.sh/ruff/
- Ruff configuration:
/pyproject.tomlunder[tool.ruff] - Changelog entry removing pylint: v4.1.0
- Performance benchmarks: https://github.com/astral-sh/ruff#benchmarks