Skip to main content

65. Ruff for Code Quality

Date: 2025-11-29

Status

Accepted

Category

Development & Tooling

Context

Python code quality historically required multiple tools:
  • black: Code formatting
  • isort: Import sorting
  • flake8: Linting (style, errors)
  • pylint: Deep static analysis
  • pyupgrade: Python version upgrades
  • autoflake: Remove unused imports
This creates:
  • Complex configuration across multiple files
  • Slow execution (multiple passes over code)
  • Conflicting rules between tools
  • Version compatibility issues

Performance Comparison (large codebase, ~50k lines)

ToolTimeRules
flake8~15s~100
pylint~45s~200
black + isort~8sformatting
ruff~0.5s800+

Decision

Use ruff (by Astral) as the single tool for linting, formatting, and import sorting.

Key Reasons

  1. Speed: 10-100x faster than alternatives (written in Rust)
  2. All-in-one: Replaces black, isort, flake8, pyupgrade, autoflake
  3. 800+ Rules: Covers flake8, pylint, pyupgrade, bandit, and more
  4. Auto-fix: Most issues can be fixed automatically
  5. pyproject.toml: Single configuration file
  6. Active Development: Rapid iteration by Astral team

Configuration

# pyproject.toml
[tool.ruff]
target-version = "py311"
line-length = 120
src = ["src", "tests"]

[tool.ruff.lint]
select = [
    "E",      # pycodestyle errors
    "W",      # pycodestyle warnings
    "F",      # pyflakes
    "I",      # isort
    "B",      # flake8-bugbear
    "C4",     # flake8-comprehensions
    "UP",     # pyupgrade
    "ARG",    # flake8-unused-arguments
    "SIM",    # flake8-simplify
    "TCH",    # flake8-type-checking
    "PTH",    # flake8-use-pathlib
    "ERA",    # eradicate (commented code)
    "PL",     # pylint
    "PERF",   # perflint
    "RUF",    # ruff-specific
]
ignore = [
    "PLR0913",  # Too many arguments (acceptable for complex APIs)
    "PLR2004",  # Magic values (too noisy)
]

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["ARG", "PLR2004"]  # Allow unused args and magic values in tests

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true

Usage

# Check for issues
ruff check src/

# Fix issues automatically
ruff check --fix src/

# Format code
ruff format src/

# Check and format in one command
ruff check --fix src/ && ruff format src/

Pre-commit Integration

# .pre-commit-config.yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.8.0
  hooks:
    - id: ruff
      args: [--fix]
    - id: ruff-format

Consequences

Positive

  • Speed: Near-instant linting, even on large codebases
  • Simplicity: One tool, one config file
  • Consistency: Same rules across all developers
  • Auto-fix: Most issues resolved automatically
  • Modern Rules: Catches Python 3.11+ improvements

Negative

  • New Tool: Team must learn ruff-specific configuration
  • Less Mature: Some edge cases not covered by flake8/pylint
  • Opinionated: Less flexibility than separate tools

Migration from Multiple Tools

# Remove old tools
pip uninstall black isort flake8 pylint autoflake pyupgrade

# Install ruff
uv add ruff --dev

# Convert configurations
# - .flake8 → pyproject.toml [tool.ruff.lint]
# - pyproject.toml [tool.black] → [tool.ruff.format]
# - pyproject.toml [tool.isort] → [tool.ruff.lint.isort]

# Run migration
ruff check --fix --unsafe-fixes src/
ruff format src/

Rule Categories

CategoryPrefixDescription
pycodestyleE, WPEP 8 style
pyflakesFUndefined names, unused imports
isortIImport sorting
flake8-bugbearBLikely bugs
flake8-comprehensionsC4Simplify comprehensions
pyupgradeUPPython version upgrades
pylintPLDeep analysis
ruff-specificRUFRuff-only rules

Alternatives Considered

black + isort + flake8

  • Rejected: Slow, multiple configurations, version conflicts
  • Three separate tools with overlapping concerns

pylint

  • Rejected: Very slow, overly strict defaults
  • Many false positives require extensive configuration

pyright/mypy (for linting)

  • Considered: Type-based linting is powerful
  • Kept alongside ruff: mypy for type checking, ruff for style/bugs

References