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 fork of 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 contendor in the universe of linters and formatters for Python code called "Ruff". It sounded promising and catched my attention, as I always keep an eye for opportunities to improve my work. Often small improvements can have a big impact on the long run.

This week I had the chance to try it out in a small project and I was instantly convinced, that this will 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
Ini

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 675 files and 27.660 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