cheat sheet
ruff
Lint and format Python code with ruff — a single Rust-powered tool that replaces flake8, isort, and black. Covers configuration, rule selection, and CI usage.
ruff — Fast Linter & Formatter
What it is
Ruff is a Rust-powered Python linter and formatter. It runs 10–100× faster than the tools it replaces and implements rules from flake8, isort, pyupgrade, pydocstyle, and many more in one binary. For new projects, ruff replaces black, flake8, isort, and pyupgrade.
Install
pip install ruff
Output: (none — exits 0 on success)
Quick example — check and auto-fix
# myfile.py (intentionally messy)
import os
import sys
x=1
y = x+1
ruff check myfile.py
Output:
myfile.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.
ruff check --fix myfile.py
ruff format myfile.py
Output:
Fixed 1 error.
1 file reformatted
When / why to use it
- New projects: use ruff instead of black + flake8 + isort separately.
- Existing projects: ruff is a drop-in replacement; configuration is near-identical to flake8/black.
- CI pipelines: its speed (milliseconds for large codebases) makes it practical even in tight pipelines.
Common pitfalls
ruff format vs ruff check —
ruff checkis the linter;ruff formatis the formatter. They are separate commands. Running onlyruff checkwill not reformat your code.
ruff check --select I --fixruns only isort-style import sorting. Useful when migrating a codebase incrementally.
Richer example — pyproject.toml configuration
[tool.ruff]
line-length = 88
target-version = "py312"
src = ["src", "tests"]
[tool.ruff.lint]
# Rule sets: E/W=pycodestyle, F=pyflakes, I=isort, B=bugbear, UP=pyupgrade, N=naming
select = ["E", "F", "I", "B", "UP", "N"]
ignore = ["E501"] # let the formatter handle line length
# Auto-fix isort violations; other rules require --fix explicitly
fixable = ["I", "F401"]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"] # allow assert in tests
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true
ruff check --fix src/
ruff format src/
Output:
Found 3 errors (3 fixed, 0 remaining).
2 files reformatted, 8 files left unchanged
Using ruff in CI (GitHub Actions)
# .github/workflows/lint.yml
- name: Lint with ruff
run: |
pip install ruff
ruff check .
ruff format --check .
ruff format --check exits non-zero if any file would be reformatted — safe for CI without modifying files.
Key rule prefixes
| Prefix | Source | Common rules |
|---|---|---|
E / W | pycodestyle | Whitespace, indentation |
F | pyflakes | Unused imports (F401), undefined names (F821) |
I | isort | Import ordering |
B | flake8-bugbear | Likely bugs and design issues |
UP | pyupgrade | Modernize Python syntax (UP007: Optional[X] → X | None) |
N | pep8-naming | Class/function naming conventions |
S | bandit | Security checks |
RUF | ruff-specific | ruff's own rules |
Check which rules are enabled
ruff rule F401 # explain a specific rule
ruff linter # list all available linters
Output:
# unused-import (F401)
Derived from the **Pyflakes** linter.
Fix is sometimes available.
## What it does
Checks for unused imports.
F Pyflakes
E pycodestyle errors
W pycodestyle warnings
I isort
N pep8-naming
D pydocstyle
UP pyupgrade
B flake8-bugbear
S flake8-bandit
ruff check vs ruff format
Ruff ships two separate sub-commands. ruff check is the linter — it analyses code for bugs, style issues, and modernisation opportunities, and (with --fix) auto-repairs many of them. ruff format is the formatter — it rewrites whitespace, line breaks, and quote style into a single canonical layout, equivalent to Black. Running one does not run the other; a full pre-commit pipeline calls both.
# Lint only (fails on violations; with --fix repairs auto-fixable ones)
ruff check src/
ruff check --fix src/
# Format only (rewrites files in place; --check fails CI without writing)
ruff format src/
ruff format --check src/
# Canonical "both" pipeline
ruff check --fix src/ && ruff format src/
Output:
Found 12 errors (10 fixed, 2 remaining).
3 files reformatted, 47 files left unchanged
In CI use
ruff check --no-fixandruff format --check— both refuse to modify files and fail with a diff when work remains. In editor save-on-format useruff check --fix --select I(sort imports) followed byruff format.
Full rule taxonomy
Ruff bundles over 800 rules across more than 50 plugins. Each rule has a prefix (the plugin family) plus a number. The prefix is what you put in select/ignore; ruff knows which underlying tool the rule originated in. The table below is the long version of what was sketched above — the categories you'll routinely opt into or out of.
| Prefix | Origin | What it covers |
|---|---|---|
F | pyflakes | Static analysis: undefined names, unused imports (F401), unused variables (F841), shadowed builtins |
E / W | pycodestyle | PEP 8 whitespace and indentation (E501 line length, E711 comparison to None) |
I | isort | Import sorting and grouping |
N | pep8-naming | Class/function/variable naming conventions |
D | pydocstyle | Docstring formatting (PEP 257) — multiple conventions (Google, NumPy, PEP 257) |
UP | pyupgrade | Modernise syntax for the target Python (UP007 rewrites Optional[X] to X | None) |
B | flake8-bugbear | Likely bugs and design smells (B008 mutable default, B904 raise-from-in-except) |
A | flake8-builtins | Shadowing of builtins (list, id, type) |
C4 | flake8-comprehensions | Unnecessary list()/dict() constructors around comprehensions |
S | flake8-bandit | Security checks (S101 assert, S301 pickle, S603 subprocess) |
T20 | flake8-print | Block print() calls in non-CLI code (T201, T203) |
SIM | flake8-simplify | Simplifications (SIM108 ternary, SIM118 key in dict) |
ARG | flake8-unused-arguments | Unused function arguments |
PTH | flake8-use-pathlib | Replace os.path calls with pathlib equivalents |
RET | flake8-return | Return-statement style (RET504 unnecessary assignment before return) |
ERA | eradicate | Commented-out code |
PL | pylint | Subset of pylint's checks — PLR refactor, PLW warning, PLE error, PLC convention |
TRY | tryceratops | Exception-handling smells (TRY003 raise vanilla exception, TRY301 raise within try) |
RUF | ruff-specific | Ruff's own rules — RUF001 ambiguous unicode, RUF013 implicit Optional |
ANN | flake8-annotations | Missing type annotations (good in lieu of full mypy on small repos) |
ASYNC | flake8-async | async/await pitfalls |
DJ | flake8-django | Django anti-patterns |
PD | pandas-vet | pandas anti-patterns (PD002 inplace, PD901 generic df name) |
NPY | NumPy-specific | NumPy deprecations |
PGH | pygrep-hooks | Catch-alls: bare noqa, blanket type: ignore, eval |
PIE | flake8-pie | Misc cleanups (PIE790 unnecessary pass, PIE796 non-unique enum values) |
Q | flake8-quotes | Quote-style enforcement (mostly redundant with ruff format) |
TCH | flake8-type-checking | Move imports into TYPE_CHECKING block |
TID | flake8-tidy-imports | Forbid relative imports or specific banned imports |
YTT | flake8-2020 | sys.version_info comparison anti-patterns |
Run ruff linter for the live list — the inventory grows every release. To learn what a specific rule does and how to fix it:
ruff rule F401
ruff rule B008 --output-format json
Output (excerpt):
# unused-import (F401)
Derived from the **Pyflakes** linter.
Fix is sometimes available.
## What it does
Checks for unused imports.
## Why is this bad?
Unused imports add a performance overhead at runtime, and can confuse readers.
...
select, ignore, extend-select — choosing rules
Ruff's selection model has three knobs. select is the base set of rule prefixes you opt into; the defaults are ["E4", "E7", "E9", "F"] — a very small list. ignore removes specific rules from that set. extend-select adds rules on top of the default — useful when you want "everything ruff turns on by default, plus a few extras" rather than re-stating the defaults.
[tool.ruff.lint]
# Replace the default selection
select = ["E", "F", "I", "B", "UP", "N", "SIM", "C4"]
# Subtract specific rules from the selection above
ignore = [
"E501", # let the formatter handle line length
"SIM108", # ternaries hurt readability sometimes
"N818", # exception name doesn't end in Error — fine for our domain types
]
# OR: keep the defaults and just add to them
extend-select = ["I", "B"]
ruff check --select F401 src/ # CLI override for one-off runs
ruff check --extend-select B src/ # default + bugbear
Output:
src/api/handler.py:3:1: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the --fix option.
Prefix matching is the trick —
select = ["E"]opts into everyE###rule. To pick exactly one, writeselect = ["E501"]. To pick a sub-family, writeselect = ["PLE"](pylint errors only, no warnings/conventions).
Per-file ignores
A [tool.ruff.lint.per-file-ignores] table relaxes specific rules for files matching a glob. Tests routinely need S101 (assert) and want longer lines than production code; CLI modules need T201 (print); __init__.py files want F401 (re-exports look like unused imports).
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "ANN", "D"] # asserts ok, untyped tests fine
"src/cli/**/*.py" = ["T201"] # print() allowed in CLI
"src/**/__init__.py" = ["F401"] # re-exports
"scripts/**/*.py" = ["T201", "S603", "S607"] # shell-out and print in scripts
"docs/conf.py" = ["A001", "INP001"]
ruff check src/ tests/ scripts/
Output:
src/api/handler.py:5:1: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the --fix option.
The test files contain plenty of assert calls — those are silenced by the per-file rule.
# noqa — line-level suppression
A trailing # noqa: <code> silences one or more rules on that line only. Always include the rule code (e.g. # noqa: E501) — a bare # noqa is itself flagged by PGH004 and is harder to audit later.
import os # noqa: F401 — re-exported from __init__.py
SECRET = "test-key-12345" # noqa: S105 — fixture, not production
def parse(data): # noqa: ANN001,ANN201 — typed in stub
return data.strip()
ruff check --add-noqa writes the suppression comments for you on every existing violation — useful when adopting a new rule set on a legacy codebase. Pair it with --select so you only mass-suppress the rules you just turned on.
ruff check --select B --add-noqa src/
Output:
Added 47 noqa directives.
The fix flag — auto-repair safely
ruff check --fix rewrites files to clear auto-fixable violations. Each rule declares whether its fix is safe (semantics-preserving, e.g. removing an unused import) or unsafe (might change behaviour, e.g. rewriting dict(a=1) to {"a": 1} when dict is shadowed). By default only safe fixes apply; --unsafe-fixes opts into the rest. --fix-only applies fixes and skips reporting un-fixable issues — useful inside a pre-commit hook.
# Apply safe fixes
ruff check --fix src/
# Apply both safe and unsafe fixes
ruff check --fix --unsafe-fixes src/
# Only apply fixes, suppress lint output
ruff check --fix-only --select I src/
# Preview what would change without writing
ruff check --diff src/
# Stat which rules would be fixed
ruff check --statistics src/
Output:
8 F401 [*] unused-import
4 I001 [*] unsorted-imports
2 B008 [*] mutable-argument-default
14
--unsafe-fixescan change runtime behaviour. Run the test suite immediately after. If your CI re-runs ruff, ensure--unsafe-fixesmatches your local invocation, or CI will flag fixes your editor applied.
fixable and unfixable lists
Restrict which rules ruff is allowed to auto-fix even when --fix is set. Useful when you want to surface (but not silently rewrite) certain categories.
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
# Allow auto-fix for these only
fixable = ["I", "F401", "UP"]
# Never auto-fix these even if the rule supports it
unfixable = ["F841", "B"]
ruff server — language server mode
ruff server starts a Language Server Protocol implementation that editors talk to over stdio. VS Code's Ruff extension, Neovim's nvim-lspconfig, Helix, and Zed all consume it. The server lints and formats on save, surfaces diagnostics inline, and provides code actions to apply individual fixes — without invoking a sub-process per keystroke.
# Manually verify the server starts (editors do this for you)
ruff server --help
Output:
Run the language server
Usage: ruff server [OPTIONS]
Options:
--preview Enable preview mode; required to use unstable features
-h, --help Print help
VS Code config to use ruff for both linting and formatting:
{
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports.ruff": "explicit",
"source.fixAll.ruff": "explicit"
}
}
}
Output: (none — settings JSON)
What ruff replaces
A single pip install ruff substitutes for the full stack a Python project typically pulled in piecemeal. The headline win is one dependency and one configuration source instead of five or six tools with overlapping responsibilities and occasionally conflicting opinions.
| Replaces | Equivalent ruff invocation |
|---|---|
flake8 + plugins (flake8-bugbear, flake8-comprehensions, …) | ruff check |
isort | ruff check --select I --fix |
pyupgrade | ruff check --select UP --fix |
autoflake (unused-import removal) | ruff check --select F401 --fix |
pydocstyle | ruff check --select D |
pep8-naming | ruff check --select N |
eradicate | ruff check --select ERA |
bandit (light coverage) | ruff check --select S |
pylint (partial — PL* rules) | ruff check --select PL |
black | ruff format |
pylint and mypy remain the two not fully covered — pylint because ruff implements a subset of its checks (call graph analysis is intentionally out of scope) and mypy because ruff doesn't do type inference at all. For most projects, ruff + mypy is the complete tooling stack.
Caching
Ruff caches results on disk so re-runs over unchanged files are near-instant. The cache lives at .ruff_cache/ by default; set cache-dir or --cache-dir to relocate it, and --no-cache to bypass entirely. The cache key includes the ruff version, the rule selection, and the file's mtime+size, so version bumps and config changes invalidate it automatically.
ruff check src/ # cold run
ruff check src/ # cached — milliseconds
ruff clean # wipe the cache directory
ruff check --no-cache src/ # bypass without wiping
Output:
Removed 1 cache files
CI/CD recipes
A handful of patterns that show up on every team's CI eventually. Run ruff before tests so a lint failure short-circuits the slower job, cache .ruff_cache/ between runs, and pin the ruff version in pyproject.toml so a release bump never silently breaks main.
GitHub Actions — minimum viable
# .github/workflows/lint.yml
name: lint
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3
with:
version: "0.6.9"
astral-sh/ruff-action installs ruff, runs ruff check, and uses GitHub's diagnostic annotations so violations show up inline in the PR diff. To also check formatting, add a second step:
- uses: astral-sh/ruff-action@v3
with:
args: "format --check"
GitHub Actions — cached, two-step
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install ruff==0.6.9
- uses: actions/cache@v4
with:
path: .ruff_cache
key: ruff-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}
- run: ruff check --output-format=github .
- run: ruff format --check .
--output-format=github emits ::error file=...,line=... directives that GitHub renders as inline PR annotations.
pre-commit integration
Ruff has a dedicated pre-commit hook repository at astral-sh/ruff-pre-commit. Pin the rev: to a specific tag — pre-commit caches the hook environment by rev, so a floating tag would re-install every run. See sections/python/pre-commit for the full hook anatomy.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
pre-commit install
pre-commit run --all-files
Output:
ruff.....................................................................Passed
ruff-format..............................................................Passed
The
ruffhook id runs the linter;ruff-formatis a separate hook id for the formatter. List linter first so its--fixoperations (notably import sorting) are then re-flowed by the formatter. Reversing the order can produce a double-pass diff that aborts the commit twice.
Common pitfalls
- Mixing
selectandextend-selectconfuses the rule set —selectreplaces the default. If you writeselect = ["B"]thinking you'll get pyflakes+bugbear, you'll actually disable F-rules. Either repeat the defaults inselector switch toextend-select. ruff checkdoesn't reformat — running onlyruff check --fixwill sort imports and remove unused ones but won't reflow whitespace. You needruff formatfor that. Hook both into pre-commit and CI.# noqawithout a code silently masks future rules —# noqaalone suppresses every rule on the line, including violations you'd want to know about later. Always write# noqa: F401so the suppression is explicit. EnablePGH004to catch barenoqa.--fixand--unsafe-fixesdisagree between editor and CI — your editor runs--fix, CI runs--fix --unsafe-fixes. CI rewrites code your editor left alone; the PR diff balloons. Set the same flag on both sides.- Cache invalidation across machines —
.ruff_cache/is platform- and version-specific. Don't commit it. CI should cache it bypyproject.tomlhash, not blindly across PRs. target-versionis not auto-detected — set it explicitly in[tool.ruff]. Without it,UPrules either rewrite syntax your runtime doesn't support or refuse to modernise code that could be.E501line length vsruff format— the formatter enforces a soft limit by re-flowing, but won't break long strings or comments. Either accept the occasional long line (ignore = ["E501"]) or run a separate string-wrapping tool.per-file-ignoresregex confusion — patterns are git-style globs, not regex.tests/**/*.pymatches files anywhere undertests/;tests/*.pymatches only the top level.- Pinning ruff in CI but not locally — leads to "works on my machine" lint diffs. Pin it in
pyproject.toml(under[project.optional-dependencies]or[tool.uv]) so both environments install the same version. ruff formatrewrites strings to double quotes — first run produces a large diff, just like Black. Usequote-style = "preserve"to keep existing quotes, or commit the bulk reformat in one go and move on.
Real-world recipes
Adopt ruff on a legacy codebase
The first ruff check --select ALL on an established repo prints thousands of errors. Adopt incrementally: turn on one rule family at a time, fix or --add-noqa the violations, commit, and tighten the next layer.
# Step 1: install and pin
pip install ruff
ruff format src/ # one-time formatting pass
git add -A && git commit -m "style: apply ruff format"
# Step 2: turn on the safe basics (F, E, W, I)
cat >> pyproject.toml <<'EOF'
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["F", "E", "W", "I"]
EOF
ruff check --fix src/
git add -A && git commit -m "lint: fix F/E/W/I violations"
# Step 3: add B (bugbear) and UP (pyupgrade), suppress what you can't fix today
ruff check --select B,UP --add-noqa src/
git add -A && git commit -m "lint: add bugbear/pyupgrade, suppress legacy noqa"
# Step 4: walk the noqa suppressions down over weeks as you touch each file
Output:
[main 1a2b3c4] style: apply ruff format
89 files changed, 482 insertions(+), 415 deletions(-)
Single config, many tools
Centralise everything ruff-related in pyproject.toml so contributors have one place to look. Includes lint selection, formatter preferences, per-file ignores, and the ruff version itself pinned under your dependency manager of choice.
[project.optional-dependencies]
dev = ["ruff==0.6.9", "mypy==1.11", "pytest>=8"]
[tool.ruff]
line-length = 100
target-version = "py312"
src = ["src", "tests"]
extend-exclude = ["migrations", "vendor", "_drafts"]
[tool.ruff.lint]
select = ["F", "E", "W", "I", "B", "C4", "UP", "SIM", "N", "S", "PTH", "RET", "TCH"]
ignore = ["E501", "S101"]
fixable = ["ALL"]
unfixable = ["F841"]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S", "ANN", "D"]
"src/cli/**/*.py" = ["T201"]
"src/**/__init__.py" = ["F401"]
"scripts/**/*.py" = ["T201", "S603", "S607"]
[tool.ruff.lint.isort]
known-first-party = ["myapp"]
combine-as-imports = true
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true
docstring-code-line-length = 80
skip-magic-trailing-comma = false
Lint on file save with editor
Modern editors call ruff server directly. The two settings that matter are formatOnSave (calls ruff format) and the codeAction list (calls ruff check --fix with selected rule scopes). The example below also runs import sorting on save.
{
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports.ruff": "explicit",
"source.fixAll.ruff": "explicit"
}
},
"ruff.lint.select": ["F", "E", "W", "I", "B", "UP"],
"ruff.format.preview": false
}
Output: (none — settings JSON)
Replace black + flake8 + isort + pyupgrade in one commit
A migration that retires four dependencies and replaces them with ruff. Run all four old tools once, lock the result, then bulk-replace the configs.
# 1. Run the existing toolchain one last time to lock in a clean baseline
black src/ tests/
isort src/ tests/
pyupgrade --py312-plus src/**/*.py
flake8 src/ tests/
git add -A && git commit -m "chore: final pass of black/isort/pyupgrade/flake8"
# 2. Remove the old tools
pip uninstall -y black isort pyupgrade flake8 flake8-bugbear
# 3. Install ruff and migrate config
pip install ruff
ruff check --select ALL --statistics src/ # see what would fire
ruff check --select F,E,W,I,B,UP --fix src/
ruff format src/
git add -A && git commit -m "chore: replace black/isort/pyupgrade/flake8 with ruff"
# 4. Update pre-commit, CI, and editor configs
Output:
Found 0 errors.
89 files left unchanged
Run ruff against only changed files (fast pre-push)
Linting the whole repo on every push is overkill once the baseline is green. Use git diff to feed only changed files to ruff — sub-second feedback even on large projects.
#!/usr/bin/env bash
# scripts/lint-changed.sh
changed=$(git diff --name-only --diff-filter=ACMR origin/main...HEAD -- '*.py')
[ -z "$changed" ] && { echo "no Python changes"; exit 0; }
echo "$changed" | xargs ruff check
echo "$changed" | xargs ruff format --check
Output: (none — exits 0 on success)
bash scripts/lint-changed.sh
Output:
src/api/handler.py:3:1: F401 [*] `os` imported but unused
Found 1 error.
Block accidental print() and breakpoint() in production code
The T20 family flags print(), and T100 flags leftover breakpoint() / pdb.set_trace(). Combine with per-file ignores so CLI scripts keep their prints. Pair with the pre-commit debug-statements hook for belt-and-braces coverage.
[tool.ruff.lint]
extend-select = ["T20", "T10"]
[tool.ruff.lint.per-file-ignores]
"src/cli/**/*.py" = ["T201"] # CLI may print
"scripts/**/*.py" = ["T201"]
ruff check src/
Output:
src/api/handler.py:18:5: T201 `print` found
src/api/handler.py:47:5: T100 Trace found: `breakpoint` used
Found 2 errors.
Enforce a specific docstring convention
Pick Google, NumPy, or PEP 257 style and turn on the D family. Ruff knows the conventions and silences rules that contradict the chosen one.
[tool.ruff.lint]
extend-select = ["D"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["D"]
def greet(name: str) -> str:
"""Return a greeting.
Args:
name: Person to greet.
Returns:
A greeting string.
"""
return f"Hello, {name}"
Combine ruff with mypy in one CI matrix
Both checkers are fast enough to run unconditionally on every PR. Run them as parallel jobs so neither blocks the other.
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3
with: { version: "0.6.9" }
- uses: astral-sh/ruff-action@v3
with: { args: "format --check", version: "0.6.9" }
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12", cache: pip }
- run: pip install mypy==1.11 -r requirements.txt
- run: mypy src/
See also
sections/python/black— the formatter ruff format is bytewise compatible with.sections/python/mypy— the static type checker ruff intentionally does not duplicate.sections/python/pre-commit— wire ruff into git so it runs on every commit.sections/python/pyproject-toml— the file where[tool.ruff]lives.sections/python/uv— fast installer;uv pip install ruffis the quickest way to get it.