FDE PulseFDE jobs open 641New in 7 days 41Companies hiring 97Remote-friendly 25%Median US pay $204kTop hirer Databricks 122
VI

The newspaper of the Forward Deployed Engineer

Tools

Ruff: lint and format a client's Python repo without a thousand-line PR

In a client's repo, a fast tool is not enough: you also have to keep the diff small, or code review turns into an argument.

Ruff: lint and format a client's Python repo without a thousand-line PR
Photo: Emile Perron / Unsplash
ShareLinkedInFacebookX

In brief

  • On a repo already formatted with Black, the Ruff formatter is more than 99.9% compatible by lines changed, and runs more than 30 times faster than Black.
  • By default, ruff check fixes nothing. Fixing requires --fix, and unsafe fixes must be enabled separately with --unsafe-fixes.
  • Use --add-noqa and per-file-ignores to defer old debt, then add the format commit's hash to .git-blame-ignore-revs in a follow-up commit so git blame stays useful.
Horizontal commit-history diagram. The first step is measuring with ruff check ., which fixes nothing and creates no commit. Four commits follow: 1 is --add-noqa, to gate new code; 2 is ruff format, run only once and highlighted in orange; 3 adds .git-blame-ignore-revs to hide the format commit from blame; 4 is ruff check --fix, applying safe fixes only.
How to adopt Ruff in a client's repo: measure first, then split the work into four small commits. The format commit stands alone so the diff is easy to review, and is then hidden from git blame. Source: Ruff official documentation, as cited in the article.

In your first week at a client, you open their Python repo and find three things at once: a Flake8 config file, an isort hook and a Black line in CI that nobody is sure still runs. You want to clean it up, but a single “standardise style” PR that touches thousands of lines is enough to make the client’s team suspicious of you straight away.

Ruff suits this situation because its formatter was designed as a replacement for Black and produces almost identical output on code already formatted with Black.

Charlie Marsh of Astral says that on such projects Ruff achieves more than 99.9% compatibility, measured by lines changed. In other words, you can swap the tool while barely changing the code.

For an FDE, this matters more than speed. A PR of a few dozen lines is one the client can read and approve. A PR of a few thousand lines is one they can only trust or reject.

One binary instead of a stack of tools

Ruff is a linter and formatter written in Rust and released under the MIT licence. It is built by Astral, the team behind uv and ty. The official documentation says Ruff can replace Flake8 plus dozens of plugins, Black, isort, pydocstyle, pyupgrade, autoflake and several other tools.

According to the same documentation, Ruff is 10 to 100 times faster than existing linters and formatters. It also has built-in caching, so unchanged files are not re-analysed. For the formatter specifically, Astral reports a speed-up of more than 30 times over Black.

In a client’s repo, that speed lets you lint the whole codebase while sitting with their team, without waiting for CI. The bigger benefit, though, is that the repo ends up with a single place for configuration instead of one file per tool.

Measure first, then four small commits

Picture an internal repo with a legacy/ directory nobody dares touch and a services/ directory under active development. The safe sequence is to measure before changing anything, then split the work into four commits, each doing one thing.

Measuring creates no commit. ruff check . lists violations without fixing anything, because Ruff only fixes when you pass the --fix flag. The output is a map of technical debt, and you can bring it to a meeting with the client.

The first commit adds the Ruff configuration and marks existing debt rather than fixing it. ruff check . --add-noqa inserts a noqa comment on every line that currently violates a rule. From then on, only new code is blocked. For directories that need a long-term exemption, use per-file-ignores in pyproject.toml:

[tool.ruff.lint.per-file-ignores]
"legacy/*" = ["E501"]

The second commit is ruff format, on its own with no other changes. Only once that commit exists do you have its hash, so the third commit is a follow-up that adds a .git-blame-ignore-revs file containing that hash, so GitHub’s blame view skips the format commit.

That way, anyone reading the history still sees who wrote each line, not who ran the formatter.

The final commit is ruff check --fix. By default Ruff only applies fixes classified as safe. Fixes that could change the code’s behaviour require --unsafe-fixes as well. In a client’s repo, keep that flag for small, individually reviewed PRs.

When does the diff still balloon?

The 99.9%-plus figure only holds for code already formatted with Black. Ruff’s documentation is explicit that the formatter deliberately differs from Black in a few places, such as blank lines at the start of a block, f-strings and single-element tuples. If the client’s repo has never used Black, the first format can produce a substantial diff.

So the first question to ask the client is “Does CI actually run Black?”, not “Which linter do you use?”. If it does, you can expect a small diff. If not, warn them in advance that there may be a large format commit, which is exactly why .git-blame-ignore-revs is needed.

The monorepo configuration trap

Ruff supports monorepos through hierarchical configuration, read from pyproject.toml, ruff.toml or .ruff.toml. One detail can easily cost you an afternoon. For each file, Ruff uses the closest configuration file in the directory tree and does not merge settings across files.

Suppose services/billing/ has its own ruff.toml only to change the line length. Every rule you set in the root pyproject.toml then stops applying to that directory. Before concluding that “Ruff is missing errors”, check which configuration is actually applied to that file.

What to learn first

Learn the measure-then-four-commits sequence before learning the rule list. The risk in a client’s repo lies in the rollout process, not in which rules you choose. After that, read the page listing the differences between Ruff and Black, because that is where you will find the answer when the client asks why a line changed.

On a CV, a line such as “migrated a Python repo from Flake8, isort and Black to Ruff, keeping the format diff under X lines without breaking git blame” is worth far more than simply listing “proficient in Ruff”.

A fast tool saves you a few minutes. A small PR the client can read and understand is what earns their trust.

Was this article useful?

Use with your AI assistantAsk Claude ↗Ask ChatGPT ↗
7 sources
Read next on the roadmap · Stage 1: FoundationsPolars and scan_csv: open a client's multi-gigabyte file without the freeze you get with pandasRunning read_csv on a large file at a client's office, then watching your laptop lock up, is embarrassing. Lazy reading helps you avoid it.