mrkeyoor.com_
Wed 05 Aug 05:01 UTC
PyPICLI & Toolingupdated 05 Aug 2026

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.

Verdict

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.

API stability3/5Still 0.x by design: minor releases add rules, promote them out of preview, and occasionally change fix behavior, so an unpinned upgrade can change lint results; the CLI and config surface themselves have been steady.
Docs5/5docs.astral.sh/ruff documents every one of the 900+ rules with rationale and examples, has a full settings reference, an FAQ mapping Flake8/Black/isort behavior, and a web playground.
Maintenance5/5Commits pushed within the hour, a paid team at Astral behind it, and rapid release cadence; the 2000+ open issues and PRs reflect enormous usage and rule requests rather than neglect.
Ecosystem5/5First-party VS Code extension, pre-commit hook, GitHub Action, and editor LSP support; adopted by major projects including FastAPI, pandas, SciPy, and Apache Airflow per the README.

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
Skip it if

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 ruff

check 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 writing

Unsafe 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 change

The 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 imports

Patterns 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-noqa

Always 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-format

Put 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@v3

Pass 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.

Alternatives

PackageRegistryPick it when
flake8PyPIYou rely on niche Flake8 plugins that Ruff has not re-implemented
blackPyPIYou only want formatting from the tool that defined the style, with years of stability behind it
pylintPyPIYou want the deepest available static analysis and will pay for it in runtime