Ruff is an extremely fast Python linter and formatter written in Rust. pip install ruff. Lint: ruff check src/. Format: ruff format src/. Fix: ruff check --fix src/. Check only: ruff format --check src/. Watch: ruff check --watch src/. Rule docs: ruff rule E501. Config in pyproject.toml: [tool.ruff] line-length=88 target-version="py312". [tool.ruff.lint] select=["E","W","F","I","N","UP","B","S","C4","PTH","RUF"]. extend-ignore=["E501","S101"]. [tool.ruff.lint.per-file-ignores] "tests/*"=["S101","S105"]. Inline: x = 1 # noqa: E501 — suppress specific rule. # noqa (no code) — suppress all rules on line (avoid). Format settings: [tool.ruff.format] quote-style="double" indent-style="space" skip-magic-trailing-comma=false. [tool.ruff.lint.isort] known-first-party=["mypackage"]. Rule families: E/W (pycodestyle), F (Pyflakes), I (isort), N (pep8-naming), UP (pyupgrade), B (flake8-bugbear), S (flake8-bandit), C4 (flake8-comprehensions), PTH (pathlib), A (flake8-builtins), ARG (unused-arguments), RET (return), SIM (simplify), TID (tidy-imports), ERA (eradicated), PL (Pylint), PERF (Perflint), RUF (Ruff-specific). --select ALL selects every rule. Unsafe fixes: ruff check --unsafe-fixes --fix src/ — applies fixes that may change semantics. ruff check --show-fixes — preview what would be auto-fixed. Exclude: [tool.ruff] exclude=[".venv","dist","migrations"]. extend-exclude=["generated_pb2.py"]. Output: ruff check src/ --format=github — GitHub Actions annotations. ruff check src/ --format=json — machine-readable. Pre-commit: - repo: https://github.com/astral-sh/ruff-pre-commit; hooks: [{id: ruff, args: ["--fix"]},{id: ruff-format}]. Claude Code generates Ruff configurations, rule sets for specific project types, and CI linting pipelines.
CLAUDE.md for Ruff
## Ruff Stack
- Version: ruff >= 0.4 | pip install ruff
- Lint: ruff check src/ | ruff check --fix (auto-repair)
- Format: ruff format src/ | ruff format --check (CI)
- Config: pyproject.toml [tool.ruff.lint] select= extend-ignore=
- Per-file: [tool.ruff.lint.per-file-ignores] "tests/*"=["S101"]
- Suppress: # noqa: E501 (specific code, never bare # noqa)
- Watch: ruff check --watch src/ for live feedback during development
Ruff Linting and Formatting Pipeline
# This file demonstrates common Ruff rules — what they catch and the fix.
# Run: ruff check src/ruff_examples.py --fix
# Format: ruff format src/ruff_examples.py
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
pass
# ─────────────────────────────────────────────────────────────────────────────
# E / W — pycodestyle style rules
# ─────────────────────────────────────────────────────────────────────────────
# E711 — comparison to None should use `is` / `is not`
# BAD: if value == None:
# GOOD:
def check_none(value: object) -> bool:
return value is None
# E712 — comparison to True/False should use `is` or `if cond:`
# BAD: if flag == True:
# GOOD:
def check_flag(flag: bool) -> bool:
return flag is True
# W291 / W293 — trailing whitespace (auto-fixed by ruff format)
# ─────────────────────────────────────────────────────────────────────────────
# F — Pyflakes rules
# ─────────────────────────────────────────────────────────────────────────────
# F401 — unused import (auto-fixable)
# BAD: import json # never used
# Ruff removes it with --fix
# F841 — local variable assigned but never used
# BAD: result = compute() # result never read
# GOOD: drop the assignment or use _
def compute_and_discard() -> None:
_ = expensive_computation() # explicit discard with _
def expensive_computation() -> int:
return 42
# F811 — redefinition of unused name
# BAD: def foo(): pass; def foo(): pass # second def shadows first
# F821 — undefined name (Pyflakes catches NameError before runtime)
# ─────────────────────────────────────────────────────────────────────────────
# I — isort import sorting
# ─────────────────────────────────────────────────────────────────────────────
# Ruff enforces isort-compatible ordering:
# 1. stdlib 2. third-party 3. first-party 4. local
# --fix reorders automatically
# Configure known-first-party in pyproject.toml:
# [tool.ruff.lint.isort]
# known-first-party = ["mypackage"]
# combine-as-imports = true
# ─────────────────────────────────────────────────────────────────────────────
# UP — pyupgrade: modernize Python syntax
# ─────────────────────────────────────────────────────────────────────────────
# UP007 — use X | Y instead of Optional[X] / Union[X, Y] (Python 3.10+)
# BAD: from typing import Optional; def f(x: Optional[int]) -> None:
# GOOD:
def process_optional(x: int | None) -> int:
return x or 0
# UP006 — use list/dict/tuple instead of typing.List/Dict/Tuple
# BAD: from typing import List; def f() -> List[int]:
# GOOD:
def get_ids() -> list[int]:
return [1, 2, 3]
# UP034 — extraneous parentheses in return
# BAD: return (x + y)
# GOOD:
def add(x: int, y: int) -> int:
return x + y
# UP035 — deprecated `typing` imports — use `collections.abc`
# BAD: from typing import Callable, Iterator
# GOOD: from collections.abc import Callable, Iterator
# ─────────────────────────────────────────────────────────────────────────────
# B — flake8-bugbear: likely bugs and design issues
# ─────────────────────────────────────────────────────────────────────────────
# B006 — mutable default argument — classic Python footgun
# BAD:
# def append_to(item, lst=[]): # B006 — shared across calls!
# lst.append(item); return lst
# GOOD:
def append_to(item: int, lst: list[int] | None = None) -> list[int]:
if lst is None:
lst = []
lst.append(item)
return lst
# B007 — loop variable unused in loop body (rename to _)
# BAD: for i in range(10): print("hello")
# GOOD:
def print_ten() -> None:
for _ in range(10):
print("hello")
# B008 — function call in default argument
# BAD: def handler(ts=datetime.now()): # evaluated once at import
# GOOD: use None and compute inside the function
# B023 — function defined in loop captures loop variable by reference
# BAD: fns = [lambda: i for i in range(3)] # all return 2
# GOOD: fns = [lambda i=i: i for i in range(3)]
# ─────────────────────────────────────────────────────────────────────────────
# C4 — flake8-comprehensions: use comprehensions where appropriate
# ─────────────────────────────────────────────────────────────────────────────
# C400 — replace list() call with list comprehension
# BAD: list(x for x in range(10))
# GOOD:
squares = [x * x for x in range(10)]
# C401 — replace set() call with set comprehension
unique_ids = {u["id"] for u in [{"id": 1}, {"id": 2}]}
# C416 — unnecessary comprehension (just use the iterable)
# BAD: [x for x in items] → list(items)
# BAD: {k: v for k, v in d.items()} → dict(d)
# ─────────────────────────────────────────────────────────────────────────────
# PTH — use pathlib instead of os.path
# ─────────────────────────────────────────────────────────────────────────────
# PTH100 — os.path.abspath → Path.resolve()
# PTH110 — os.path.exists → Path.exists()
# PTH118 — os.path.join → Path / operator
# PTH119 — os.path.basename → Path.name
# GOOD:
def read_config(config_path: str) -> str:
path = Path(config_path).resolve()
if not path.exists():
raise FileNotFoundError(f"Config not found: {path}")
return path.read_text(encoding="utf-8")
# ─────────────────────────────────────────────────────────────────────────────
# S — flake8-bandit security rules (subset, not all)
# ─────────────────────────────────────────────────────────────────────────────
# S101 — use of assert (stripped by -O optimization)
# Ruff flags this — typically suppressed in test files:
# per-file-ignores: "tests/*" = ["S101"]
# S105 — hardcoded password string
# BAD: API_KEY = "sk-abc123"
# GOOD:
API_KEY = os.environ.get("API_KEY", "")
# S603 / S607 — subprocess without shell / starting process
# GOOD:
import subprocess
def run_safe(args: list[str]) -> str:
result = subprocess.run(args, capture_output=True, text=True,
check=True, shell=False)
return result.stdout
# ─────────────────────────────────────────────────────────────────────────────
# RUF — Ruff-specific rules
# ─────────────────────────────────────────────────────────────────────────────
# RUF001/002/003 — ambiguous unicode characters in string/docstring/comment
# RUF005 — use unpacking instead of concatenation: [*a, *b] vs a + b
combined = [*[1, 2], *[3, 4]] # RUF005 fix
# RUF010 — use explicit conversion flag in f-strings:
# BAD: f"{str(x)}" → f"{x!s}"
# GOOD:
def format_value(x: object) -> str:
return f"{x!s}"
# RUF012 — mutable class var should be annotated with ClassVar
from typing import ClassVar
class Registry:
_handlers: ClassVar[dict[str, object]] = {} # RUF012 — must be ClassVar
# ─────────────────────────────────────────────────────────────────────────────
# SIM — flake8-simplify
# ─────────────────────────────────────────────────────────────────────────────
# SIM102 — use single if instead of nested if
# BAD: if a: if b: do()
# GOOD:
def nested_condition(a: bool, b: bool) -> None:
if a and b:
print("both true")
# SIM108 — use ternary instead of if-else assignment
# BAD: if cond: x = a else: x = b
# GOOD:
def ternary(cond: bool, a: int, b: int) -> int:
return a if cond else b
# SIM117 — merge nested with statements
# BAD: with open(f1) as a: with open(f2) as b:
# GOOD:
def read_two(f1: str, f2: str) -> tuple[str, str]:
with Path(f1).open() as a, Path(f2).open() as b:
return a.read(), b.read()
# ─────────────────────────────────────────────────────────────────────────────
# pyproject.toml configuration reference
# ─────────────────────────────────────────────────────────────────────────────
PYPROJECT_RUFF = """
[tool.ruff]
line-length = 88
target-version = "py312"
exclude = [
".venv", ".nox", "dist", "build",
"migrations", "*_pb2.py", "conftest.py",
]
[tool.ruff.lint]
# Start with a focused set; add rule families as team comfort grows
select = [
"E", "W", # pycodestyle
"F", # Pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"PTH", # use pathlib
"SIM", # simplify
"RUF", # Ruff-specific
]
extend-ignore = [
"E501", # line too long — formatter handles this
"N818", # Exception suffix — not always wanted
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101", "S105", "S106", "ARG001", "ARG002"]
"migrations/*"= ["E501", "N999"]
"scripts/*" = ["T201"] # allow print()
[tool.ruff.lint.isort]
known-first-party = ["mypackage"]
combine-as-imports = true
force-sort-within-sections = true
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
"""
if __name__ == "__main__":
print("Ruff linting and formatting examples")
print("Run: ruff check src/ruff_examples.py --fix")
print(" ruff format src/ruff_examples.py")
print("\nKey rule families:")
for family, desc in [
("E/W", "pycodestyle — spacing, blank lines"),
("F", "Pyflakes — unused imports, undefined names"),
("I", "isort — import order"),
("UP", "pyupgrade — modern Python syntax"),
("B", "bugbear — mutable defaults, loop capture"),
("C4", "comprehensions — replace list()/set() calls"),
("PTH", "pathlib — replace os.path usage"),
("SIM", "simplify — ternary, nested if, nested with"),
("RUF", "Ruff-specific — unicode, ClassVar, f-strings"),
]:
print(f" {family:6} {desc}")
For the flake8 + isort + pyupgrade + Black alternative — running four separate tools requires four config files (.flake8, .isort.cfg, a pyupgrade pre-commit invocation, and pyproject.toml [tool.black]), four separate pre-commit hooks each spawning a Python process, and four different # noqa / # type: ignore syntaxes, while Ruff replaces all four with a single process that runs in under 100 ms on a 100k-line repo because it is implemented in Rust and processes files in parallel — and ruff check --fix applies isort sorting, pyupgrade syntax modernization, and bugbear fixes in one pass. For the pylint alternative — pylint has broader code-smell detection (too-many-branches, too-many-arguments) but is 10–100× slower than Ruff because it builds a full AST call graph, and every pylint suppression requires a # pylint: disable= comment while Ruff’s # noqa: B006 code is self-documenting — Ruff is the right first-pass linter for CI speed; pylint can run as a scheduled deeper check. The Claude Skills 360 bundle includes Ruff skill sets covering pyproject.toml rule selection, E/W/F/I/UP/B/C4/PTH/SIM/RUF rule families, per-file-ignores patterns, —fix and —unsafe-fixes workflows, isort configuration, ruff format Black-compatible settings, pre-commit hook setup, GitHub Actions CI with —format=github, noqa inline suppression, and ruff rule EXXXX lookup. Start with the free tier to try linting and formatting code generation.