Modernise the dev toolchain with Ruff

How to replace Flake8, isort and brunette with a single tool

Geschrieben von Timo Rieber am 22. Dezember 2023

For years I used a combination of isort, brunette (a wrapper around black) and Flake8 to lint and format my code with only little changes to their default configuration, held together within setup.cfg:

[flake8]
max-line-length = 79
max-complexity = 6
per-file-ignores =
  src/application/migrations/*.py: C901

[isort]
force_alphabetical_sort_within_sections = true
line_length = 79
lines_after_imports = 2
multi_line_output = 3
no_lines_before = LOCALFOLDER
profile = black

[tool:brunette]
line-length = 79
single-quotes = true
Ini

I was very happy with it. Some time ago I came across a new contender in the universe of linters and formatters for Python code called "Ruff". It sounded promising and caught my attention, as I always keep an eye for opportunities to improve my work.

This week I had the chance to try it out in a small project and I was instantly convinced that this would be my new toolchain for linting and formatting Python code. I took a few iterations how to configure it to my needs and it was a breeze to use. Why? In my opinion the main advantages are:

  • much faster than my current tools (saves me and the CI pipeline minutes)
  • single, integrated tool and a drop-in replacement for Flake8, isort and brunette
  • highly configurable within a single file

In addition to the existing benefits of the tools I used before, Ruff has the capability to fix common findings automatically:

  • removes unused imports
  • trims empty lines and spaces, even in multiline strings
  • removes unused f-string syntax

With the prepared configuration in the pyproject.toml I was then able to replace the same toolchain in a much larger project within a few minutes:

[tool.ruff]
fix = true
line-length = 79
select = [
  "C90", # lint.mccabe
  "E",  # lint.pycodestyle
  "F", # lint.pyflakes
  "I", # lint.isort
  "W", # lint.pycodestyle
]
src = ["src", "tests"]
target-version = "py311"

[tool.ruff.format]
quote-style = "single"

[tool.ruff.lint.isort]
known-first-party = ["tests"]
lines-after-imports = 2

[tool.ruff.lint.mccabe]
max-complexity = 6
Toml

The transition was very smooth. And the promise of being a drop-in replacement was kept: the task to lint and format the whole project with 623 files and 30.468 lines of Python code resulted in only 143 lines added and 127 lines removed. Most of it automatically and most of it because of some whitespace changes in multiline strings.

The commands could not be simpler:

# Format "src" recursively
ruff format src

# Lint and automatically fix "src" recursively
ruff check src
Bash