ruff
Ruff is a Python linter and code formatter written in Rust that consolidates a whole toolbox into one binary: it replaces Flake8 (and dozens of its plugins), isort, pydocstyle, pyupgrade, and autoflake for linting, and Black for formatting. The pitch is speed with numbers behind it: 10-100x faster than the tools it replaces, with built-in caching so unchanged files are skipped. It ships over 900 rules, reads configuration from pyproject.toml or ruff.toml, supports cascading config for monorepos, and is maintained by Astral, the company behind uv.
The correct default for Python linting and formatting in 2026; the speed changes how you use lint, not just how long it takes. Pin the version, curate your rule selection, and keep expectations straight: it replaces your linter and formatter, not your type checker.
Use it if
- Your lint step is slow enough that people skip it locally: Ruff runs on large codebases in well under a second, fast enough for save-hooks and pre-commit
- You are tired of coordinating Flake8 + isort + Black + pyupgrade versions and configs: one tool, one config table, one version to pin
- You want autofixes: ruff check --fix removes unused imports, upgrades syntax, and sorts imports mechanically
- You work in a monorepo: hierarchical config discovery means each subproject can tighten or relax rules without a wrapper script
- You depend on a Flake8 plugin with no Ruff re-implementation: Ruff has no plugin system, so a rule either exists in the binary or you keep Flake8 running alongside
- You need pylint-depth analysis: Ruff covers a growing share of pylint rules but does not do the deeper cross-module inference pylint attempts, and it is not a type checker at all
- Unpinned CI must stay green: Ruff is still 0.x and new minors add or recategorize rules, so an unpinned version bump can fail builds that passed yesterday
- Your team wants tooling with no commercial angle: Ruff is developed by a VC-funded company (Astral); the tool is MIT and free, but the roadmap follows a business
Setup reality
Installation is genuinely easy: uv add --dev ruff, pip install ruff, or a standalone installer script; it is a single static binary with no Python dependencies, so it never conflicts with your project's packages. The real setup work is configuration: defaults are conservative (roughly Flake8's F and a subset of E rules), so getting the advertised value means curating a select list of rule families in pyproject.toml and deciding what to ignore. Migrating an old codebase means an initial flood of violations, so plan a --add-noqa pass or a formatting commit. Pin the exact version in CI and pre-commit; minor releases change rule behavior while the project is pre-1.0.
Patterns
Install and run linter plus formatterinstall-run
# one-off, no install:
uvx ruff check # lint current directory
uvx ruff format # format current directory
# or add to the project:
uv add --dev ruff
# or: pip install ruffcheck and format are separate commands with separate concerns; running format never fixes lint violations and vice versa.
Base configuration in pyproject.tomlpyproject-config
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "B", "UP"]
ignore = ["E741"]Lint settings live under [tool.ruff.lint], not [tool.ruff]; the old top-level keys are deprecated and warn.
Pick rule families beyond the defaultsselect-rules
[tool.ruff.lint]
select = [
"F", # pyflakes
"E", # pycodestyle errors
"I", # isort (import sorting)
"B", # flake8-bugbear
"UP", # pyupgrade
"SIM", # flake8-simplify
"S", # flake8-bandit (security)
]Defaults are intentionally minimal; most of Ruff's value is in families you opt into, and each rule's docs page says whether its fix is safe.
Fix violations automaticallyautofix
ruff check --fix # apply safe fixes
ruff check --fix --unsafe-fixes # also apply fixes that may change behavior
ruff check --diff # preview fixes without writingUnsafe fixes can alter runtime behavior (e.g. removing a seemingly unused import that had side effects); review that diff.
Format like Black, check in CIformat-code
ruff format # write formatting
ruff format --check # CI: exit nonzero if anything would change
ruff format --diff # show what would changeThe formatter is designed as a drop-in Black replacement with a documented list of tiny deviations; do not run both on the same repo.
Use Ruff as isortsort-imports
# enable the I rules, then:
ruff check --select I --fix
[tool.ruff.lint.isort]
known-first-party = ["myapp"]Import sorting is a lint rule (I001) with a fix, not part of ruff format; teams miss this and wonder why format leaves imports alone.
Relax rules for tests and __init__.pyper-file-ignores
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"] # allow assert in tests
"__init__.py" = ["F401"] # allow re-export importsPatterns are glob-matched against the path relative to the config file; quoting the keys is required TOML.
Suppress a single violation inlinesuppress-inline
import legacy_module # noqa: F401
# retrofitting an old codebase: annotate everything currently failing
# ruff check --add-noqaAlways give noqa a rule code; bare noqa hides future unrelated violations on the same line, and RUF100 can flag unused noqa comments.
Run in pre-commitpre-commit-hook
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.1
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-formatPut ruff-check with --fix before ruff-format so fixes get formatted; the rev pin is your version pin, keep it exact.
Lint in GitHub Actionsgithub-action
name: Ruff
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3Pass version: "0.16.1" to the action to match local and pre-commit versions, or CI and laptops will disagree about violations.
Exclude generated and vendored codeexclude-paths
[tool.ruff]
extend-exclude = [
"migrations",
"proto_gen",
"*_pb2.py",
]Use extend-exclude to keep Ruff's sensible default excludes (.git, .venv, build) instead of replacing them with exclude.