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

bash
pip install ruff

Output: (none — exits 0 on success). Wheels ship pre-compiled Rust binaries for the common platforms.

bash
uv add --dev ruff

Output: dependency added to the dev group in pyproject.toml

bash
poetry add --group dev ruff

Output: updated lockfile + dev install

bash
pipx install ruff

Output: installed to isolated venv, ruff CLI on PATH (recommended for global, repo-spanning use)

bash
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.y semver-ish.
  • The 0.x prefix 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 --preview flag 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-sh GitHub 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 in pyproject.toml).
  • ruff-lsp — separate package, deprecated in favour of the built-in ruff server.
  • astral-sh/ruff-pre-commit — the upstream pre-commit hook repo.

Alternatives

PackageTrade-off
flake8 + pluginsThe old standard. Slower, requires a dozen plugins for the same coverage. Mature.
pylintDeeper semantic analysis (e.g. unused-variable across modules). Slower; opinionated. Use alongside ruff for stricter codebases.
blackFormat-only. Reference implementation that ruff format aims to match.
isortSort-imports-only. Absorbed into ruff (I rules).
pyflakesTiny static-analysis library that flake8 wraps. Ruff reimplements its rules in Rust.
pydocstyle / pyupgrade / bugbearAll absorbed into ruff rule families (D, UP, B).

Common gotchas

  1. --preview rules 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.
  2. ruff format vs black is 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.
  3. 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_cache if results disappear after upgrading.
  4. select/extend-select/ignore precedence is non-obvious. The right mental model is: start from select, add extend-select, subtract ignore. extend-ignore is gone — modern config just uses ignore.
  5. per-file-ignores patterns are globs, not regex. __init__.py matches any init file in any directory; tests/**/* matches everything under tests/. Quote them in pyproject.toml to avoid TOML parser surprises.
  6. 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 sdist which won't compile without a Rust toolchain.
  7. ruff check --fix rewrites files. This is the point — but stage your changes first or use --diff to preview. The --unsafe-fixes flag 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:

PrefixSourceRule family
FPyflakesUndefined names, unused imports, syntax surface checks
E, WpycodestyleWhitespace, line length, indentation (pep8)
IisortImport sorting and grouping
Npep8-namingClass/function/variable naming conventions
DpydocstyleDocstring style (Google, NumPy, PEP 257)
UPpyupgradeModernise syntax to the targeted Python
Bflake8-bugbearCommon bugs (mutable defaults, unused-loop-variables)
Aflake8-builtinsShadowing builtins
C4flake8-comprehensionsWasteful list/dict comprehensions
SIMflake8-simplifyRefactor patterns (if x: return True else: return False)
TCHflake8-type-checkingMove type-only imports under TYPE_CHECKING
RUFRuff-nativeRules that don't have an upstream tool
PLpylintA subset of pylint's most-valuable checks
ANNflake8-annotationsAnnotation completeness
Sflake8-banditSecurity smells (eval, weak crypto)
ERAeradicateCommented-out code
PERFperflintLoop-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:

toml
[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:

ini
# .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:

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

bash
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

toml
[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:

lua
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-cache disables the cache. Useful when debugging spurious behaviour after a version bump. Don't ship it to production.
  • exclude cuts work. Excluding node_modules, .venv, generated dirs cuts scan time proportionally.
  • ruff check --watch runs incrementally as files change. The watch loop reuses the cache aggressively.
  • output-formatconcise (default) and full differ in print cost on huge violation sets. CI logs benefit from --output-format=github which produces annotation-style output GitHub renders inline.
  • --statistics prints 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.
  • --preview is 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-ignoreignore was the most disruptive recent example.

Upgrade pattern:

  1. Pin exactly: ruff==0.6.4 in pyproject.toml and .pre-commit-config.yaml.
  2. Bump on a deliberate cadence (monthly or per-minor). Read the changelog — especially the "Removed rules" and "Behaviour changes" sections.
  3. Run ruff check --no-cache --output-format=full on the bump PR to catch newly-enabled defaults or shifted rule codes.
  4. 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:

toml
[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

SymptomCauseFix
error: Selection contains unknown rule codeRule removed or renamed in this ruff versionCheck the changelog for the new code; remove if obsolete.
ruff: command not found after installpip installed to a venv not on PATHActivate the venv, or pipx install ruff for global.
ruff check --fix produced no changes despite obvious violationThe rule is non-fixable, or --unsafe-fixes is requiredCheck ruff linter output: [*] marks fixable rules.
ruff format output diverges from blackEdge cases in chained calls, magic-trailing-comma handlingPick one tool; both produce almost identical output but mixing creates churn.
Cache stale after version bumpRare bug — usually cache invalidates on binary hashrm -rf .ruff_cache and retry.
Config not picked upWrong 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 violationsDefault ruleset includes opinionated/conflicting rulesAdd 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 by ruff format. Don't run both.
  • isort — replaced by ruff's I rules.
  • flake8 + plugins — replaced by ruff's E/W/F/B/... rules.
  • pyupgrade — replaced by ruff's UP rules.
  • pydocstyle — replaced by ruff's D rules.
  • pylint — partially replaced (subset in PL family). 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-commitastral-sh/ruff-pre-commit is the canonical hook source.
  • Editor LSPsruff server is 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

yaml
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.x releases 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 check and format --check steps — fail on either independently; the developer sees which class of issue blocked the PR.
  • uv tool install instead of pip 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 --preview off, 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