| name | ruff |
| description | Use this skill when linting, formatting, or fixing Python code with ruff. Activates on mentions of ruff, ruff check, ruff format, ruff fix, ruff server, ruff analyze, Python linting, Python formatting, flake8, isort, black replacement, noqa, pycodestyle, pyflakes, lint rules, ruff config, ruff.toml, autofix, per-file-ignores, language server for Python, or Python code quality. |
ruff: Python Linter & Formatter
ruff (v0.15.12, Apr 2026) is three tools in one Rust binary: linter (ruff check), formatter (ruff format), and dependency analyzer (ruff analyze graph). It replaces Flake8, Black, isort, pyupgrade, and dozens more.
The built-in language server (ruff server) replaces the deprecated ruff-lsp package (archived Dec 2025).
Invocation
uv run ruff ...
uvx ruff ...
ruff ...
Rule Selection: The Critical Decision
Default rules are minimal: only ["E4", "E7", "E9", "F"], catches syntax errors and undefined names but misses most quality rules. You almost certainly need to extend this.
select vs extend-select
| Command | Behavior |
|---|
select = ["E", "F", "B"] | Replaces entire default set. Only these run. |
extend-select = ["B"] | Adds to whatever select provides (or defaults) |
Config inheritance trap: When a child config specifies select, the parent's ignore list is discarded. This surprises people with monorepo setups.
Specificity wins: More specific prefixes override less specific ones. select = ["E"] + ignore = ["E501"] enables all E rules except E501.
Recommended Selection Strategy
New project, start broad:
[tool.ruff.lint]
select = [
"E", "W",
"F",
"I",
"N",
"UP",
"B",
"SIM",
"TC",
"RUF",
]
ignore = ["E501"]
Library / open source, maximum strictness:
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"W191", "E111", "E114", "E117",
"D206", "D300",
"Q000", "Q001", "Q002", "Q003", "Q004",
"COM812", "COM819",
"D203", "D213",
"D100", "D104",
"ANN101", "ANN102",
"FBT", "ERA001",
"E501",
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101", "D", "ANN", "ARG"]
"scripts/**" = ["T20", "INP001"]
"**/__init__.py" = ["F401", "D104"]
Legacy migration, incremental:
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F"]
extend-select = [
"I",
"UP",
]
The ALL Selector
select = ["ALL"] enables every stable rule. Ruff auto-disables conflicting pairs (D203/D211, D212/D213), but being explicit is better practice. Preview rules require preview = true and are not included even with ALL.
Formatter Behavior
Configuration
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
docstring-code-format = true
preview = false
Rules That CONFLICT With the Formatter
When using ruff format, these lint rules should be avoided:
ignore = [
"W191", "E111", "E114", "E117",
"D206", "D300",
"Q000", "Q001", "Q002", "Q003", "Q004",
"COM812", "COM819",
]
Also avoid ISC002 in Ruff's documented formatter-conflict case: ISC002 selected, ISC001 not selected, and flake8-implicit-str-concat.allow-multiline = false.
Known Deviations from Black
Ruff targets >99.9% parity with Black but has 23 intentional divergences. The most impactful:
| Deviation | Ruff | Black |
|---|
| F-string interiors | Formats {expr} contents (stable since 0.9.0) | Does not touch f-string interiors |
Pragma comments (# noqa, # type:) | Excluded from line width | Counted in line width |
| Implicit string concat | Merges when fits on one line | Splits more aggressively |
| Blank lines at block start | Removes them | Preserves them (Black 24+) |
| Trailing comments | Expands statement to keep comment close | Collapses, moves comment to end |
| Single-element tuples | Always parenthesizes | Removes parens when safe |
E501 and the Formatter
The formatter makes best-effort line wrapping, it cannot always succeed. Comments, long strings, and URLs may exceed line-length. Either ignore E501 or set lint.pycodestyle.max-line-length higher than line-length.
Fix Safety Model
ruff check --fix .
ruff check --fix --unsafe-fixes .
ruff check --fix --diff .
| Safety | Meaning | Example |
|---|
| Safe | Cannot change runtime behavior | Reordering imports |
| Unsafe | May change behavior | list(x)[0] -> next(iter(x)) changes exception type |
Override per-rule:
[tool.ruff.lint]
extend-safe-fixes = ["RUF015"]
extend-unsafe-fixes = ["F401"]
Suppression System
import os
LONG_VALUE = "..."
ruff check --select RUF100 --fix .
ruff check --add-noqa .
Preview Mode
Preview is a staging area for new rules and formatter changes.
[tool.ruff.lint]
preview = true
explicit-preview-rules = true
Preview rules are NOT activated by prefix selection or ALL, they require preview mode enabled. Use explicit-preview-rules = true to control which preview rules activate individually.
Dependency Graph Analysis
ruff analyze graph src/
ruff analyze graph --direction=dependents src/
ruff analyze graph --detect-string-imports src/
Use cases: selective test running, dead code detection, circular import detection.
Configuration
File precedence: .ruff.toml > ruff.toml > pyproject.toml (nearest wins, no merging across levels).
Falls back to ~/.config/ruff/ruff.toml when no project config exists.
[tool.ruff]
target-version = "py312"
line-length = 88
src = ["src", "tests"]
required-version = "==0.15.12"
extend = "../pyproject.toml"
[tool.ruff.lint.isort]
known-first-party = ["myproject"]
combine-as-imports = true
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.flake8-type-checking]
runtime-evaluated-base-classes = ["pydantic.BaseModel"]
runtime-evaluated-decorators = ["attrs.define"]
For the complete rule catalog snapshot, see references/rules.md.
For full configuration reference, see references/configuration.md.
Debugging
ruff check --show-settings .
ruff check --show-files .
ruff check --statistics .
ruff rule E501
ruff linter
Non-Obvious Gotchas
| Gotcha | Explanation |
|---|
| TCH -> TC rename | TCH prefix is now legacy alias for TC. Use TC in new configs |
| No third-party plugins | Ruff re-implements Flake8 plugins in Rust. Cannot install additional ones |
| isort differences | Some edge cases differ from real isort (aliased imports, inline comments) |
| Notebooks: per-cell scope | E402 checked per-cell, not per-file. Each cell is its own module scope |
--fix can break code | Even "safe" fixes can break dynamic Python. Review diffs for F401, UP, B rules |
ruff-lsp is dead | Use ruff server (built into binary). The separate ruff-lsp package was archived Dec 2025 |
| Range formatting | ruff format --range=10:1-20:1 formats only lines 10-20 (single file, not notebooks) |
Anti-Patterns
| Anti-Pattern | Fix |
|---|
Blanket # noqa on every line | Fix the violations or use per-file-ignores |
select = ["ALL"] with no ignore | Always pair with formatter conflict rules and overly strict rules |
Running ruff format before ruff check --fix | Lint fixes first (may reorder imports), then format |
Using ruff-lsp | Switch to ruff server (built-in, maintained) |
| Ignoring E501 without using formatter | Either use ruff format OR enforce E501, not neither |
select in child config without knowing it resets | Use extend-select to preserve parent's rule set |
What This Skill is NOT
- Not a replacement for
ruff --help or ruff rule <CODE> for specific rule docs
- Not for type checking (use ty)
- Not for package management (use uv)
- Not for third-party Flake8 plugins that ruff hasn't re-implemented