cheat sheet
ruff
Package-level reference for ruff on PyPI — install variants, fast-moving version policy, and how a single binary subsumes flake8, isort, pyupgrade, and black.
ruff
What it is
ruff is a Rust-powered linter and formatter for Python, created by Charlie Marsh and developed at Astral (the same team behind uv). It implements rules ported from flake8, isort, pyupgrade, pydocstyle, pep8-naming, bugbear, and many more — all in a single static binary that runs ~10–100× faster than the Python tools it replaces.
In 2026 ruff has effectively become the default Python lint+format tool: it's bundled into most new project templates and gradually displacing flake8/isort/black toolchains.
Install
pip install ruff
Output: (none — exits 0 on success). Wheels ship pre-compiled Rust binaries for the common platforms.
uv add --dev ruff
Output: dependency added to the dev group in pyproject.toml
poetry add --group dev ruff
Output: updated lockfile + dev install
pipx install ruff
Output: installed to isolated venv, ruff CLI on PATH (recommended for global, repo-spanning use)
curl -LsSf https://astral.sh/ruff/install.sh | sh
Output: standalone installer — drops the binary at ~/.local/bin/ruff without involving pip.
Versioning & Python support
- Ruff is on a fast release cadence — typically every 1–2 weeks. Versions are
0.x.ysemver-ish. - The
0.xprefix is deliberate — the project pre-commits to not being API-stable while rules and config keys evolve. Pin exact versions in CI. - Recent releases support Python 3.7+ as a target. The ruff binary itself is statically compiled — no Python interpreter required to run.
- The
--previewflag opts into rules and behaviour scheduled to land in a future stable release. Preview rules can change or be removed between releases.
Package metadata
- Maintainer: Astral (
astral-shGitHub org) - Project home: github.com/astral-sh/ruff
- Docs: docs.astral.sh/ruff
- PyPI: pypi.org/project/ruff
- License: MIT
- Governance: corporate-stewarded (Astral) with active community contribution
- First released: August 2022
- Downloads: hundreds of millions per month; one of the fastest-growing packages on PyPI
Optional dependencies & extras
Ruff is a single static binary with no Python or system dependencies — and no PyPI extras. Everything is built in: linter, formatter, import-sorter, the language server (ruff server), and the ruff check / ruff format subcommands.
That single-binary design is part of the value proposition — installing ruff never resolves into 12 transitive packages the way flake8 + plugins does.
Related Astral packages that compose with ruff:
uv— the project/python installer (works alongside ruff inpyproject.toml).ruff-lsp— separate package, deprecated in favour of the built-inruff server.astral-sh/ruff-pre-commit— the upstream pre-commit hook repo.
Alternatives
| Package | Trade-off |
|---|---|
flake8 + plugins | The old standard. Slower, requires a dozen plugins for the same coverage. Mature. |
pylint | Deeper semantic analysis (e.g. unused-variable across modules). Slower; opinionated. Use alongside ruff for stricter codebases. |
black | Format-only. Reference implementation that ruff format aims to match. |
isort | Sort-imports-only. Absorbed into ruff (I rules). |
pyflakes | Tiny static-analysis library that flake8 wraps. Ruff reimplements its rules in Rust. |
pydocstyle / pyupgrade / bugbear | All absorbed into ruff rule families (D, UP, B). |
Common gotchas
--previewrules churn. Preview rules can be renumbered, renamed, or removed between releases. Don't depend on them in CI without pinning ruff exactly and reading each release note.ruff formatvsblackis almost identical but not byte-identical. Edge cases around chained method calls, comment placement, and complex expressions can produce a one-line diff. Pick one tool per repo and don't run both.- Cache invalidation on rule version bumps. Ruff caches results under
.ruff_cache/. After a version bump the cache should invalidate by hashing the binary, but stale-cache reports do appear —rm -rf .ruff_cacheif results disappear after upgrading. select/extend-select/ignoreprecedence is non-obvious. The right mental model is: start fromselect, addextend-select, subtractignore.extend-ignoreis gone — modern config just usesignore.per-file-ignorespatterns are globs, not regex.__init__.pymatches any init file in any directory;tests/**/*matches everything undertests/. Quote them inpyproject.tomlto avoid TOML parser surprises.- The Rust binary is platform-specific. PyPI ships wheels for x86_64 / arm64 Linux, macOS, and Windows. On rare platforms (e.g. musl Alpine, some BSDs) you fall back to
sdistwhich won't compile without a Rust toolchain. ruff check --fixrewrites files. This is the point — but stage your changes first or use--diffto preview. The--unsafe-fixesflag enables fixers that may change runtime behaviour; treat it as off-by-default for a reason.
Plugin & rule ecosystem
Ruff has no plugin system — every rule ships in the binary. Rules are grouped into namespaces by prefix, each derived from an upstream tool:
| Prefix | Source | Rule family |
|---|---|---|
F | Pyflakes | Undefined names, unused imports, syntax surface checks |
E, W | pycodestyle | Whitespace, line length, indentation (pep8) |
I | isort | Import sorting and grouping |
N | pep8-naming | Class/function/variable naming conventions |
D | pydocstyle | Docstring style (Google, NumPy, PEP 257) |
UP | pyupgrade | Modernise syntax to the targeted Python |
B | flake8-bugbear | Common bugs (mutable defaults, unused-loop-variables) |
A | flake8-builtins | Shadowing builtins |
C4 | flake8-comprehensions | Wasteful list/dict comprehensions |
SIM | flake8-simplify | Refactor patterns (if x: return True else: return False) |
TCH | flake8-type-checking | Move type-only imports under TYPE_CHECKING |
RUF | Ruff-native | Rules that don't have an upstream tool |
PL | pylint | A subset of pylint's most-valuable checks |
ANN | flake8-annotations | Annotation completeness |
S | flake8-bandit | Security smells (eval, weak crypto) |
ERA | eradicate | Commented-out code |
PERF | perflint | Loop-perf antipatterns |
ruff linter lists every rule with its prefix. New families land in --preview before promotion to stable.
Selection grammar: select = ["E", "F", "B"] enables three families. select = ["ALL"] enables every rule (always paired with extensive ignore — total chaos otherwise). The extend-select key adds to a base; ignore subtracts. Per-file overrides live under per-file-ignores:
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
ignore = ["E501"] # line length — handled by formatter
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "ANN"] # asserts and annotations not enforced in tests
"__init__.py" = ["F401"] # re-exports are fine
"migrations/**/*.py" = ["ALL"] # auto-generated; do not touch
Custom plugins are not supported. The closest workaround is external rules — codes ruff doesn't recognise are passed through unchanged so external tooling (pylint, vulture) can still report them in a unified # noqa ecosystem.
Real-world recipes
Migrating from flake8 + isort + pyupgrade
Existing toolchain config:
# .flake8
[flake8]
max-line-length = 100
extend-ignore = E203, W503
exclude = .venv,migrations
# .isort.cfg
[settings]
profile = black
line_length = 100
Migrated to ruff alone in pyproject.toml:
[tool.ruff]
line-length = 100
exclude = [".venv", "**/migrations/**"]
[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B"]
ignore = ["E203", "W503"]
[tool.ruff.lint.isort]
known-first-party = ["mypkg"]
Then delete .flake8, .isort.cfg, and the flake8, isort, pyupgrade entries from requirements-dev.txt. Drop ~6 deps, gain ~50× speed.
Auto-fix with safety guards
ruff check --fix --diff src/
Output: prints the diff that --fix would apply without writing. Review, then re-run without --diff. The --unsafe-fixes flag enables behaviour-changing fixers — gated behind opt-in for a reason; never wire it into pre-commit.
Per-directory rule overrides
[tool.ruff.lint.per-file-ignores]
"src/legacy/**/*.py" = ["ANN", "D", "UP"]
"src/new/**/*.py" = [] # full strictness on greenfield code
"scripts/*.py" = ["T20"] # print() allowed in CLI scripts
This is how a monorepo migrates progressively — legacy code keeps the old looseness, new code gets the full ruleset.
Editor LSP via ruff server
Most editors auto-discover ruff server from the project venv. For Neovim:
require("lspconfig").ruff.setup({
init_options = {
settings = {
args = { "--select", "E,F,I,UP" },
},
},
})
The LSP runs ruff check and ruff format on save, surfacing diagnostics in the editor without a separate terminal pane.
Performance tuning
Ruff is fast by default — the question is mostly don't undo it:
- Cache lives in
.ruff_cache/. Hashed by file content + ruff version + config. Add to.gitignore. CI runners benefit from caching this directory. --no-cachedisables the cache. Useful when debugging spurious behaviour after a version bump. Don't ship it to production.excludecuts work. Excludingnode_modules,.venv, generated dirs cuts scan time proportionally.ruff check --watchruns incrementally as files change. The watch loop reuses the cache aggressively.output-format—concise(default) andfulldiffer in print cost on huge violation sets. CI logs benefit from--output-format=githubwhich produces annotation-style output GitHub renders inline.--statisticsprints rule-level counts. Useful for triaging which rules to enable progressively without flooding the log.
For benchmarking comparisons, ruff is roughly 30–100× faster than flake8+isort+pyupgrade combined. On a 100k-line codebase, ruff finishes in under a second; flake8 takes ~30 s.
Version migration guide
Ruff is on a fast cadence — typically a release every 1–2 weeks. Pre-1.0 means rules may move or be renamed between releases. The conventions:
- Renames are aliased. Old codes redirect to new ones with a deprecation warning. The old code keeps working for ~6 months.
--previewis the staging area. Rules there can change shape, codes can be reassigned, defaults can flip. Don't pin to preview rules in production CI without exact ruff pinning.- Promotion from preview to stable is announced in release notes; sometimes a rule's default changes from "warn" to "fix on default-
--fix". - Config-key renames are deprecation-warned for one minor release before removal.
extend-ignore→ignorewas the most disruptive recent example.
Upgrade pattern:
- Pin exactly:
ruff==0.6.4inpyproject.tomland.pre-commit-config.yaml. - Bump on a deliberate cadence (monthly or per-minor). Read the changelog — especially the "Removed rules" and "Behaviour changes" sections.
- Run
ruff check --no-cache --output-format=fullon the bump PR to catch newly-enabled defaults or shifted rule codes. - For preview rules in production: pair the exact pin with explicit
select = ["E", "F", ..., "PREVIEW_RULE"]and treat each ruff bump as a code review.
Astral tracks long-term API stability in a public RFC repo. Until ruff hits 1.0, treat every minor bump as a potential breaking change.
Configuration & layout patterns
Ruff's config lives in pyproject.toml under [tool.ruff] (top-level options like line-length, target-version, exclude) and [tool.ruff.lint]/[tool.ruff.format] (rule selection and formatting). The legacy .ruff.toml and ruff.toml are also supported but pyproject.toml is canonical:
[tool.ruff]
target-version = "py312"
line-length = 100
extend-exclude = ["generated/", "vendor/"]
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "TCH"]
ignore = ["E501"]
fixable = ["ALL"]
unfixable = ["B"] # bugbear suggestions are not auto-applied
[tool.ruff.lint.isort]
known-first-party = ["mypkg"]
combine-as-imports = true
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
docstring-code-format = true # format code blocks inside docstrings
Monorepo strategy: a root pyproject.toml with shared base settings; each sub-project's pyproject.toml uses extend = "../pyproject.toml" to inherit and override. Ruff walks up the directory tree to resolve config — but unlike black, the resolution is file-local, so a file in packages/api/src/foo.py gets packages/api/pyproject.toml.
For a per-directory ignore pattern, prefer per-file-ignores (glob-keyed) over scattered # noqa comments. The lint config in one place is much easier to audit.
Troubleshooting common errors
| Symptom | Cause | Fix |
|---|---|---|
error: Selection contains unknown rule code | Rule removed or renamed in this ruff version | Check the changelog for the new code; remove if obsolete. |
ruff: command not found after install | pip installed to a venv not on PATH | Activate the venv, or pipx install ruff for global. |
ruff check --fix produced no changes despite obvious violation | The rule is non-fixable, or --unsafe-fixes is required | Check ruff linter output: [*] marks fixable rules. |
ruff format output diverges from black | Edge cases in chained calls, magic-trailing-comma handling | Pick one tool; both produce almost identical output but mixing creates churn. |
| Cache stale after version bump | Rare bug — usually cache invalidates on binary hash | rm -rf .ruff_cache and retry. |
| Config not picked up | Wrong pyproject.toml selected (monorepo path traversal) | Run ruff check --show-files to see which config applies per file. |
select = ["ALL"] floods CI with thousands of violations | Default ruleset includes opinionated/conflicting rules | Add explicit ignore for D, ANN, PLR, T20 etc, or scope select to specific families. |
ruff check --show-settings dumps the fully-resolved config (including inherited values). Compare against expectation when something doesn't behave as configured.
Ecosystem integrations
Ruff replaces several tools wholesale and complements the rest:
black— replaced byruff format. Don't run both.isort— replaced by ruff'sIrules.flake8+ plugins — replaced by ruff'sE/W/F/B/... rules.pyupgrade— replaced by ruff'sUPrules.pydocstyle— replaced by ruff'sDrules.pylint— partially replaced (subset inPLfamily). Pylint's deeper semantic analysis still wins for some checks; keep both if needed.mypy— complementary. Ruff catches lint; mypy catches type errors. Both ship as pre-commit hooks.pre-commit—astral-sh/ruff-pre-commitis the canonical hook source.- Editor LSPs —
ruff serveris the in-binary LSP;ruff-lsp(separate package) is deprecated. - GitHub Action
astral-sh/ruff-action— runs ruff against PR and posts annotations.
CI integration
name: lint
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv tool install ruff==0.6.4
- run: ruff check --output-format=github src/ tests/
- run: ruff format --check src/ tests/
Key choices:
- Exact pin — rule churn in
0.xreleases means floating versions cause week-to-week CI noise. --output-format=github— emits GitHub-style annotations so violations show inline in the PR's "Files changed" tab.- Separate
checkandformat --checksteps — fail on either independently; the developer sees which class of issue blocked the PR. uv tool installinstead ofpip install— fastest install path on a cold runner. ~200 ms vs ~3 s.- No matrix — ruff output is identical across Python versions (it's a Rust binary).
For monorepos, scope to changed paths the same way as black/mypy: git diff --name-only origin/main -- '*.py' piped to xargs ruff check.
When NOT to use this
- Hand-tuned legacy style that disagrees with ruff's defaults across many rules. Migration is feasible but the PR diff is large; consider
darker-style incremental adoption. - Pure-Python tool builds shipped to PyPI where adding a ~10 MB Rust binary as a dev dep is undesirable. (Rare — ruff wheels are well-cached.)
- Platforms without prebuilt ruff wheels (musl on uncommon architectures, some BSDs). You fall back to sdist + Rust toolchain — usually painful.
- CI that hard-blocks if any rule moves — ruff's preview rules churn. Either pin
--previewoff, or pin ruff exactly and bump deliberately. - Pylint-deep semantic analysis — ruff doesn't replicate pylint's cross-module unused-variable detection. Keep pylint alongside if you need it.
See also
- Python: ruff — full rule-selection guide, configuration recipes, editor integration
- Packages: pip-black — the formatter ruff format aims to match
- Packages: pip-pre-commit — run ruff on every commit
- Packages: pip-mypy — pair ruff (lint) with mypy (types)