astral-sh / ruff
Ruff folds Python linting and formatting into one fast, versioned policy
Ruff is a Python linter and formatter written in Rust. It reimplements rules from Flake8 and its plugins, isort, pyupgrade and others, and formats code to match Black's style. Speed is what people notice first; the lasting change is that one configuration file and one pinned version can define a repository's whole lint and style policy.
WHAT TO KNOW FIRST
- Ruff reimplements over 900 lint rules in Rust, and its formatter targets Black: the docs report more than 99.9 percent of lines formatted identically on Black-formatted projects such as Django and Zulip.
- Fixes are labeled safe or unsafe; Ruff applies only safe fixes by default, and unsafe fixes need --unsafe-fixes next to --fix.
- Ruff is not yet on semantic versioning: minor releases can change stable rules, the default rule set and formatter style, so the version belongs in the lockfile and the hook configuration.
01One binary in place of a chain of tools
The README describes Ruff as an extremely fast Python linter and code formatter written in Rust, able to replace Flake8 with dozens of its plugins, Black, isort, pydocstyle, pyupgrade and autoflake. Every rule, whichever tool it first came from, is reimplemented as a first-party feature. Built-in caching skips unchanged files, configuration lives in pyproject.toml, ruff.toml or .ruff.toml, and first-party editor integrations exist for VS Code and other editors.
The README lists Apache Airflow, Apache Superset, FastAPI, Hugging Face Transformers, pandas and SciPy among projects that use it. Speed changes habits: when a check finishes before anyone notices it running, it can run on every save and in a commit hook without someone switching it off. The longer-lasting effect is consolidation. One tool means one version to pin, one configuration to review and one set of diagnostics shared by the editor, the hook and CI.
02Rule selection is the real configuration
Ruff supports over 900 rules, organized in categories identified by short codes such as F, E, B, UP and RUF. The README says the default rule set draws on those categories among others and leaves out stylistic rules that overlap with a formatter. The linter docs recommend lint.select over lint.extend-select so the active set is explicit, and they describe a preview arrangement of rules into categories such as correctness, suspicious, complexity, performance and style.
Treat the selection as policy, not as a list to maximize. Start from the defaults, add a category when it prevents a kind of defect the team actually ships, and write a short reason beside unusual choices. Per-file ignores belong where generated code or tests need different treatment. The docs advise against enabling the pedantic or restriction groups wholesale, since those rules are opinionated or deliberately restrictive, and suggest choosing individual rules from them instead.
03Safe fixes and unsafe fixes
Ruff can correct many violations itself and labels every fix as safe or unsafe. According to the linter docs, safe fixes are meant to preserve runtime behavior and only remove comments when deleting entire statements or expressions, such as an unused import, while unsafe fixes can change runtime behavior, remove comments, or both. By default Ruff applies only safe fixes. Unsafe ones need the unsafe-fixes setting or the --unsafe-fixes flag next to --fix, and Ruff prints a hint when unsafe fixes are available but not enabled.
Keep that line in automation. A pre-commit hook or CI job that rewrites code should apply safe fixes only; unsafe fixes should be run by a person who reads the diff. Even safe fixes deserve a look during the first rollout, because a large automatic change hides small surprises. Make the adoption commit purely mechanical and separate from feature work, so reviewers can focus on the configuration and a sample of changed files.
- Use ruff check without --fix as the CI gate and reserve writing modes for hooks and local runs.
- Enable unsafe fixes per rule, by hand, after reading what the rule changes.
- Run the test suite after any bulk fix, since a clean lint report is not evidence of correct behavior.
04A formatter that targets Black, with stated deviations
ruff format is designed as a drop-in replacement for Black. The formatter docs say it aims for near-identical output on Black-formatted code, and that on large Black-formatted projects such as Django and Zulip more than 99.9 percent of lines come out the same. Like Black it offers little style configuration, though it does let a project choose quote style, indent style and line endings. ruff format --check reports files that would change without writing them.
The same page says the formatter is not meant to be used interchangeably with Black on an ongoing basis, because it differs in a few deliberate ways listed as known deviations. Choose one formatter per repository. When moving from Black, run the formatter once on a dedicated branch, commit the result on its own, and add that commit to the blame ignore file if the team keeps one. After that, formatting should disappear from code review.
05Configuration discovery in larger repositories
Ruff reads pyproject.toml, ruff.toml or .ruff.toml; inside pyproject.toml every section takes a tool.ruff prefix, so [lint] becomes [tool.ruff.lint]. The README describes hierarchical, cascading configuration as monorepo-friendly. With nothing configured, the defaults exclude common directories such as .git, .venv, build, dist and node_modules, use a line length of 88 and an indent width of 4, assume Python 3.11 as the target version, and allow fixes for all enabled rules when --fix is passed.
In a repository with several packages, make inheritance visible. A root configuration should hold shared policy, and package-level files should state only what differs and why. Run Ruff from the repository root and from a package directory to confirm the same files are checked either way, since editors and task runners often start in different working directories. When local and CI results differ, compare the Ruff version first and then the configuration file that was actually discovered.
06Versioning is not semantic yet
Ruff's versioning page states that the project does not yet have a stable API and uses its own scheme: the minor version carries breaking changes and the patch version carries bug fixes. Minor releases can promote rules to stable, change the behavior or scope of stable rules, add rules to or remove them from the default set, promote safe fixes and change the formatter's stable style. The GitHub releases API listed 0.17.2, published October 22, 2026, as the latest release when this guide was checked.
The consequence is simple: an unpinned Ruff can turn a passing build red, or reformat files, with no change in the repository. Pin the version in development dependencies, in the pre-commit rev and in the CI action, and upgrade on purpose. Read the changelog for each minor release, run ruff check and ruff format --check across the codebase, and commit any new fixes or formatting as a change of their own. Preview mode, turned on with preview = true or --preview, opts into unstable rules and style early.
07Editor, hook and CI should agree
The README shows three integration points. The ruff-pre-commit repository provides ruff-check and ruff-format hooks with a pinned rev. The VS Code extension and other editor setups provide diagnostics while editing. The ruff-action GitHub Action runs Ruff in CI. Installation routes include uvx for one-off runs, uv tool install for a global tool, uv add --dev for a project dependency, pip, pipx, Homebrew, Conda and standalone installer scripts for macOS, Linux and Windows.
Choose one route per context and keep the versions aligned. A developer's global Ruff that is newer than the project's pinned version will report different results and waste review time. Point editors at the project-local binary and let CI be the authority. If the team uses the standalone installer, read the script first and pin a version, which the README shows with a versioned install URL.
08Where Ruff stops
Ruff analyzes source without running it. Some rules catch likely bugs, but it does not replace a type checker, a test suite or a security review. Astral, which backs Ruff, also makes uv and the type checker ty, and the uv guide on this site covers the environment side of the same toolchain. The repository is released under the MIT license.
Adopt Ruff when its rules cover the defects the team cares about and one pinned tool can retire several. Keep a specialist tool where a custom plugin enforces something Ruff cannot express, and say so in the configuration. Judge the rollout by stable policy and fewer style comments in review, not by the number of packages removed from the development dependencies.
A SENSIBLE FIRST HOUR
Start small enough to learn the repo
- Try it without installing anything, as the README shows: uvx ruff@0.17.2 check lints the current directory and uvx ruff@0.17.2 format formats it.
- Add it to the project with uv add --dev ruff (or pip install ruff), then run ruff check and ruff format --check on a clean branch and keep the output for comparison.
- Write the rule selection into pyproject.toml under [tool.ruff.lint], starting from the defaults, apply safe fixes with ruff check --fix, and review the diff before committing.
- Use the same pinned version in pre-commit through ruff-pre-commit, whose README example pins rev v0.17.2, and in CI through astral-sh/ruff-action, so local and CI results match.
SOURCE LEDGER
What this review is built on
We use the project repository and first-party documentation. Access, licenses and project direction can change, so recheck the linked source before making a production decision.