
Ruff
- 16 installs
- 25 repo stars
- Updated July 31, 2026
- hyperb1iss/hyperskills
Lints, formats, and fixes Python code with ruff, a single Rust binary replacing Flake8, Black, isort, and pyupgrade, plus its built-in language server.
About
Covers linting, formatting, and analyzing Python with ruff. A developer uses it for ruff check/format/fix, configuring ruff.toml, noqa/per-file-ignores, or the built-in ruff server.
- Three tools in one: check, format, analyze graph
- Built-in ruff server replaces deprecated ruff-lsp
Ruff by the numbers
- 16 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #195 of 290 Python skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hyperb1iss/hyperskills --skill ruffAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16 |
|---|---|
| repo stars | ★ 25 |
| Last updated | July 31, 2026 |
| Repository | hyperb1iss/hyperskills ↗ |
What it does
Lints, formats, and fixes Python code with ruff, a single Rust binary replacing Flake8, Black, isort, and pyupgrade, plus its built-in language server.
Files
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 ... # Project dependency (pinned version)
uvx ruff ... # One-off (latest)
ruff ... # Global installRule 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", # pycodestyle
"F", # Pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"TC", # flake8-type-checking
"RUF", # Ruff-specific
]
ignore = ["E501"] # Let formatter handle line lengthLibrary / open source, maximum strictness:
[tool.ruff.lint]
select = ["ALL"]
ignore = [
# Formatter conflicts (MUST disable)
"W191", "E111", "E114", "E117",
"D206", "D300",
"Q000", "Q001", "Q002", "Q003", "Q004",
"COM812", "COM819",
# Pydocstyle conflicts
"D203", "D213",
# Overly strict
"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", # Step 1: import sorting (safe, auto-fixable)
"UP", # Step 2: pyupgrade (mostly auto-fixable)
# "B", # Step 3: uncomment when ready
]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" # "double" | "single" | "preserve"
indent-style = "space" # "space" | "tab"
skip-magic-trailing-comma = false
docstring-code-format = true # Format code in docstrings
preview = false # Enable 2026 style guideRules That CONFLICT With the Formatter
When using ruff format, these lint rules should be avoided:
ignore = [
"W191", "E111", "E114", "E117", # Indentation
"D206", "D300", # Docstring formatting
"Q000", "Q001", "Q002", "Q003", "Q004", # Quotes
"COM812", "COM819", # Commas
]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 . # Safe fixes only
ruff check --fix --unsafe-fixes . # Include unsafe (review first!)
ruff check --fix --diff . # Preview changes before applying| 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"] # Promote to safe
extend-unsafe-fixes = ["F401"] # Demote to unsafe (require --unsafe-fixes)Suppression System
# Line-level
import os # noqa: F401
# Block-level (new in 0.15.0)
# ruff: disable[E501]
LONG_VALUE = "..."
# ruff: enable[E501]
# File-level
# ruff: noqa: F401, E501ruff check --select RUF100 --fix . # Clean up unused noqa comments
ruff check --add-noqa . # Auto-add noqa to all violationsPreview Mode
Preview is a staging area for new rules and formatter changes.
[tool.ruff.lint]
preview = true # Expands defaults from 59 to 412 rules
explicit-preview-rules = true # Require individual opt-in even with preview onPreview 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/ # File dependency graph (JSON)
ruff analyze graph --direction=dependents src/ # Reverse graph
ruff analyze graph --detect-string-imports src/ # Include dynamic importsUse 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" # Inferred from requires-python if unset
line-length = 88
src = ["src", "tests"] # First-party import classification
required-version = "==0.15.12" # Pin version with a PEP 440 specifier
extend = "../pyproject.toml" # Inherit parent config
[tool.ruff.lint.isort]
known-first-party = ["myproject"]
combine-as-imports = true
[tool.ruff.lint.pydocstyle]
convention = "google" # "google" | "numpy" | "pep257"
[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 . # Dump resolved config
ruff check --show-files . # List files that would be checked
ruff check --statistics . # Count violations per rule
ruff rule E501 # Explain a specific rule
ruff linter # List all available lintersNon-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 --helporruff 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
Ruff Configuration Reference
File Precedence
.ruff.toml > ruff.toml > pyproject.toml (nearest wins, no merging across directory levels)
User-level fallback: ~/.config/ruff/ruff.toml (Linux/macOS) or ~\AppData\Roaming\ruff\ruff.toml (Windows)
pyproject.toml vs ruff.toml
# pyproject.toml # ruff.toml (no [tool.ruff] prefix)
[tool.ruff] line-length = 100
line-length = 100
[lint]
[tool.ruff.lint] select = ["E", "F"]
select = ["E", "F"]
[format]
[tool.ruff.format] quote-style = "single"
quote-style = "single"
[lint.isort]
[tool.ruff.lint.isort] known-first-party = ["mymod"]
known-first-party = ["mymod"]Config Inheritance
[tool.ruff]
extend = "../pyproject.toml" # Inherit parent config, override locallyTop-Level Settings
[tool.ruff]
target-version = "py312" # py37-py315, inferred from requires-python if unset
line-length = 88
indent-width = 4
src = ["src", "tests"] # First-party import classification
required-version = "==0.15.12" # Pin version with a PEP 440 specifier
respect-gitignore = true
include = ["*.py", "*.pyi"]
exclude = [".venv", "migrations"]
extend-include = ["*.ipynb"]
extend-exclude = ["generated"]
per-file-target-version = { "legacy/**" = "py38" }Lint Settings
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F"] # Baseline (replaces defaults)
extend-select = ["B", "I"] # Additive
ignore = ["E501"] # Subtractive
fixable = ["ALL"] # Which rules can be auto-fixed
unfixable = ["F401"] # Block auto-fix for these
extend-safe-fixes = ["RUF015"] # Promote to safe
extend-unsafe-fixes = ["F401"] # Demote to unsafe
preview = false
explicit-preview-rules = false # Require individual preview opt-in
task-tags = ["TODO", "FIXME", "XXX", "HACK"]
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
external = ["V"] # Preserve in noqa for external tools
logger-objects = ["myapp.logging.logger"]
typing-modules = ["myapp.types"]Per-File Ignores
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401", "E402"]
"**/{tests,docs}/*" = ["S101", "D"]
"**/conftest.py" = ["ARG"]
"scripts/**" = ["T20", "INP001"]
"*.ipynb" = ["T20", "E402"]
"migrations/**" = ["E501"]Lint Sub-Settings
isort
[tool.ruff.lint.isort]
known-first-party = ["myproject"]
known-third-party = ["wandb"]
combine-as-imports = true
force-single-line = false # WARNING: conflicts with formatter
lines-after-imports = 2
required-imports = ["from __future__ import annotations"]pydocstyle
[tool.ruff.lint.pydocstyle]
convention = "google" # "google" | "numpy" | "pep257"pylint
[tool.ruff.lint.pylint]
max-args = 5
max-returns = 6
max-branches = 12
max-statements = 50
max-locals = 15
max-nested-blocks = 5
max-bool-expr = 5
max-public-methods = 20
allow-magic-value-types = ["int", "str"]mccabe
[tool.ruff.lint.mccabe]
max-complexity = 10flake8-type-checking
[tool.ruff.lint.flake8-type-checking]
runtime-evaluated-base-classes = ["pydantic.BaseModel"]
runtime-evaluated-decorators = ["attrs.define"]
quote-annotations = false
strict = falseflake8-tidy-imports
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "parents"
banned-api = { "os.path.join" = { msg = "Use pathlib.Path instead" } }flake8-import-conventions
[tool.ruff.lint.flake8-import-conventions.aliases]
numpy = "np"
pandas = "pd"
matplotlib.pyplot = "plt"
seaborn = "sns"
polars = "pl"flake8-pytest-style
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = falseflake8-builtins
[tool.ruff.lint.flake8-builtins]
ignorelist = ["id", "type"]pycodestyle
[tool.ruff.lint.pycodestyle]
max-doc-length = 88
max-line-length = 120 # Set higher than line-length for E501 tolerance
ignore-overlong-task-comments = falsepyflakes
[tool.ruff.lint.pyflakes]
allowed-unused-imports = ["myapp.compat"]Format Settings
[tool.ruff.format]
quote-style = "double" # "double" | "single" | "preserve"
indent-style = "space" # "space" | "tab"
line-ending = "auto" # "auto" | "lf" | "cr-lf" | "native"
skip-magic-trailing-comma = false
docstring-code-format = false
docstring-code-line-length = "dynamic"
preview = false
exclude = []Analyze Settings
[tool.ruff.analyze]
detect-string-imports = false
direction = "dependencies" # "dependencies" | "dependents"
type-checking-imports = true
include-dependencies = {}Suppression Comments
import os # noqa: F401 # Line-level
# ruff: disable[E501] # Block-level (0.15.0+)
# ruff: enable[E501]
# ruff: noqa: F401, E501 # File-level
# isort: skip_file # isort file skip
# isort: off / # isort: on # isort block
# fmt: off / # fmt: on # Format block
a = [1,2,3] # fmt: skip # Format skip (statement-level)Format Suppression in Markdown (Preview)
<!-- fmt:off -->
<!-- fmt:on -->
<!-- blacken-docs:off -->
<!-- blacken-docs:on -->isort Action Comments
# isort: skip_file
# isort: off / # isort: on
# isort: skip # Skip next import
# isort: split # Force section break
# ruff: isort: skip_file # Ruff-prefixed variantRuff Rule Family Map (Apr 2026)
Use ruff linter and <https://docs.astral.sh/ruff/rules/> for authoritative current counts. This file is a routing map for deciding which rule families matter, not a version-pinned census.
Framework-Specific
| Prefix | Linter | Rules | Fixable | Focus |
|---|---|---|---|---|
| AIR | Airflow | 10 | 5 | DAG args, Airflow 3 migration |
| DJ | flake8-django | 7 | 0 | Model/form anti-patterns |
| FAST | FastAPI | 3 | 3 | Route/dependency issues |
| NPY | NumPy-specific | 4 | 3 | Deprecated APIs, legacy random |
| PD | pandas-vet | 13 | 1 | .inplace, .values, etc. |
Core Python
| Prefix | Linter | Rules | Fixable | Focus |
|---|---|---|---|---|
| E/W | pycodestyle | 67 | 46 | PEP 8 style |
| F | Pyflakes | 43 | 11 | Undefined names, unused imports |
| N | pep8-naming | 16 | 2 | Naming conventions |
| D | pydocstyle | 47 | 30 | Docstring conventions |
| DOC | pydoclint | 7 | 0 | Docstring/signature mismatches (all preview) |
| UP | pyupgrade | 47 | 46 | Version upgrade opportunities |
| I | isort | 2 | 2 | Import sorting |
| C90 | mccabe | 1 | 0 | Cyclomatic complexity |
Pylint Re-implementations
| Prefix | Category | Rules | Fixable |
|---|---|---|---|
| PLC | Convention | 16 | 7 |
| PLE | Error | 38 | 10 |
| PLR | Refactor | 33 | 13 |
| PLW | Warning | 28 | 9 |
Flake8 Plugin Re-implementations
| Prefix | Linter | Rules | Fixable | Focus |
|---|---|---|---|---|
| A | flake8-builtins | 6 | 0 | Shadowing built-in names |
| ANN | flake8-annotations | 11 | 5 | Missing type annotations |
| ARG | flake8-unused-arguments | 5 | 0 | Unused function arguments |
| ASYNC | flake8-async | 15 | 3 | Blocking calls in async |
| B | flake8-bugbear | 43 | 13 | Bug patterns, design issues |
| BLE | flake8-blind-except | 1 | 0 | Bare except: |
| C4 | flake8-comprehensions | 19 | 18 | Unnecessary comprehensions |
| COM | flake8-commas | 3 | 2 | Trailing commas |
| CPY | flake8-copyright | 1 | 0 | Copyright headers (preview) |
| DTZ | flake8-datetimez | 10 | 0 | Naive datetime usage |
| EM | flake8-errmsg | 3 | 3 | String literals in exceptions |
| EXE | flake8-executable | 5 | 1 | Shebang/permission issues |
| FA | flake8-future-annotations | 2 | 2 | from __future__ import annotations |
| FBT | flake8-boolean-trap | 3 | 0 | Boolean positional arguments |
| FIX | flake8-fixme | 4 | 0 | TODO/FIXME/XXX/HACK |
| G | flake8-logging-format | 8 | 2 | Logging format issues |
| ICN | flake8-import-conventions | 3 | 1 | Unconventional aliases |
| INP | flake8-no-pep420 | 1 | 0 | Missing __init__.py |
| INT | flake8-gettext | 3 | 0 | i18n/gettext patterns |
| ISC | flake8-implicit-str-concat | 4 | 3 | Implicit string concat |
| LOG | flake8-logging | 7 | 5 | Logging misuse |
| PIE | flake8-pie | 8 | 7 | Misc code smells |
| PT | flake8-pytest-style | 31 | 13 | pytest best practices |
| PTH | flake8-use-pathlib | 35 | 28 | os.path -> pathlib |
| PYI | flake8-pyi | 55 | 29 | Type stub issues |
| Q | flake8-quotes | 5 | 5 | Quote consistency |
| RET | flake8-return | 8 | 8 | Return patterns |
| RSE | flake8-raise | 1 | 1 | Unnecessary parens in raise |
| S | flake8-bandit | 73 | 0 | Security issues |
| SIM | flake8-simplify | 30 | 26 | Simplification opportunities |
| SLF | flake8-self | 1 | 0 | Private member access |
| SLOT | flake8-slots | 3 | 0 | Missing __slots__ |
| T10 | flake8-debugger | 1 | 0 | Debugger imports |
| T20 | flake8-print | 2 | 2 | print() statements |
| TC | flake8-type-checking | 9 | 8 | TYPE_CHECKING optimization |
| TD | flake8-todos | 7 | 1 | TODO format |
| TID | flake8-tidy-imports | 4 | 2 | Banned/relative imports |
Note: TCH is a legacy alias for TC. Both work, prefer TC in new configs.
Other Tool Re-implementations
| Prefix | Linter | Rules | Fixable | Replaces |
|---|---|---|---|---|
| ERA | eradicate | 1 | 0 | Commented-out code detection |
| FLY | flynt | 1 | 1 | f-string conversion |
| FURB | refurb | 36 | 36 | Code modernization (all fixable) |
| PGH | pygrep-hooks | 5 | 2 | Blanket type: ignore, eval |
| PERF | Perflint | 6 | 4 | Performance anti-patterns |
| TRY | tryceratops | 10 | 2 | Exception handling |
| YTT | flake8-2020 | 10 | 0 | sys.version comparison |
Ruff-Specific
| Code | Name | Notable |
|---|---|---|
| RUF100 | Unused # noqa directive | The "yesqa replacement" |
| RUF102 | Invalid rule code in suppression | New in 0.15.0 |
| RUF103 | Invalid suppression comment syntax | New in 0.15.0 |
| RUF104 | Unmatched suppression comment | New in 0.15.0 |
| RUF060 | in against empty collection | Stabilized 0.15.0 |
| RUF037 | Unnecessary empty iterable |
Rules Stabilized in Recent Releases
0.15.0 (Feb 2026)
ASYNC212/240/250 (blocking calls in async), B912 (map without strict), UP042 (replace StrEnum), FURB110/171, RUF060/061/064, RUF102-104
0.13.0 (Sep 2025)
AIR002/301/302/311/312 (Airflow 3 migration), UP050, FURB116, RUF043/059
0.12.0 (Jul 2025)
UP045/046/047/049 (PEP 604/695 syntax), PLR1733, PLW0177/1641, FURB122/132/157/162/166, RUF028/049/053/057/058
Conflicting Rule Pairs
| Pair | Resolution |
|---|---|
| D203 vs D211 | Choose D211 (no blank line before class docstring) |
| D212 vs D213 | Choose D212 (docstring starts on first line) |
| COM812 vs formatter | Disable COM812 when using ruff format |
| ISC002 vs formatter | Avoid when ISC002 is selected, ISC001 is not selected, and allow-multiline is false |