Definition
A Python project grows quickly, and small flaws pile up without anyone noticing: an import that no longer serves any purpose after a refactor, a variable never read again, a line written in a way PEP 8 advises against. None of this crashes the program, but the file ends up hard for anyone other than its author to read.
That is exactly what ruff is meant to catch: it reads the code without running it and reports what is off. That kind of reading has a name, linting, and it existed long before ruff. What is new comes down to two things: written in Rust, it crosses a project in a fraction of a second where its predecessors needed several minutes, and it gathers into a single binary what used to require four or five separate programs.
Two commands are enough, run from the project root.
pip install ruff
ruff check .The command walks every file in the folder and prints one line per problem, with the file, the position and a rule code. Nothing is changed until you ask for it. Installation goes through pip, inside the project's virtual environment.
What it sees, and what it will never see
An example beats an abstract explanation. Here is a file that runs without crashing, and yet contains three flaws that ruff will catch.
import os
import json
def load(path):
data = json.load(open(path))
total = 0
return dataruff reports the useless os import (rule F401), the total variable computed for nobody (F841), and the file never closed: three flaws invisible at runtime, which cost readability later and leave resources open.
A natural question follows: does ruff catch everything that can go wrong? No: its analysis is static, it reads the text of the program without ever running it. The table below separates what it can spot from what escapes it.
| It spots this | It cannot know this |
|---|---|
| An import unused or written twice | That a list will be empty and raise an IndexError |
| A name used before it exists, a future NameError | That a key will be missing from a dictionary when it is read |
| A comparison written the wrong way round | That your discount calculation applies the wrong percentage |
This limit follows from what ruff is meant to do quickly. It explains why ruff replaces neither pytest, which checks what the code produces once run, nor mypy, which confronts type hint declarations with one another. These three tools answer different questions.
The formatter, next to black
Spotting flaws is only half of what ruff does. Under a separate command, ruff format, it rewrites the layout, without touching the meaning of the program. That ground belongs to black, whose style it borrows almost character for character.
Is it worth switching, for anyone already using black? On an existing black project, ruff format does not change how the code looks, only the speed and the number of tools to maintain. On a fresh project, ruff alone spares you two settings to keep in sync. On a team attached to one precise option, a trial on a branch is worth running before deciding.
Keep this distinction in mind: ruff check answers "what might cost you dearly?", ruff format answers "what should the code look like?". Uneven indentation is not a bug, a dead import is never settled by moving spaces around.
The setting that decides everything
A classic mistake is switching on every available rule family at once, then running the tool on a project that has been live for months. The terminal hands back thousands of warnings, most of them stylistic preferences nobody ever asked for, and the tool ends up disabled before the week is out.
The approach that works: keep the default rule set, which reports only the real problems, then add one family at a time, fixing what it flags.
| Family | What it brings |
|---|---|
F | The real flaws: dead imports, useless variables, unknown names |
E | The layout gaps inherited from the official style guide |
I | The sorting and grouping of imports at the top of a file |
UP | The turns of phrase made obsolete by recent Python versions |
The setting lives in pyproject.toml, under [tool.ruff.lint]. The configuration becomes shared by the whole team and by the continuous integration machine. A versioned configuration file always beats an option typed by hand, which half the team's machines will eventually forget.
Frequently asked questions
Should black be kept once ruff is already in use?
No, it is no longer necessary: ruff ships its own formatter, and running both tools amounts to rewriting the same file twice for an almost identical result. Keeping the older tool only makes sense if the configuration relies on an option ruff does not expose yet.
How should the hundreds of errors shown on the first run be handled?
Certainly not by fixing them all on the same day: a commit touching five hundred files makes the project history unreadable and drowns the real changes. The method that works is to run ruff check --fix, which settles the mechanical part on its own, then to narrow the rule set down to what the team is ready to fix now.
Can the automatic fixes break code that used to work?
The fixes applied by default are called safe: they never change the behaviour of the program. Others, marked as risky, only apply with an extra option, and those can remove a comment or move a default value. Never run an automatic fix on a working folder that has not been saved, so the changes can be reviewed and undone in one gesture.