Ruff Python: The 0.18-Second Linter That Replaced Five Tools in One

A complete guide to installing, configuring, and migrating to Ruff — the Rust-powered Python linter and formatter that replaces Flake8, Black, isort, and more at 46x the speed.

Updated 10 min read
Python code displayed on a monitor — hero image for Ruff Python linter guide

Ruff is a Python linter and code formatter, written in Rust, that replaces Flake8, Black, isort, pyupgrade, and autoflake with a single tool and one config block. On a 47,000-line, 180-file production codebase, Ruff lints in 0.18 seconds: 46× faster than Flake8 and 262× faster than Pylint. As of June 2026, it draws ~262 million monthly PyPI downloads and runs inside FastAPI, Hugging Face Transformers, Pandas, and SciPy.

This guide covers installation, tiered rule configuration, the correct linting-before-formatting workflow, VS Code and GitHub Actions setup, migrating from legacy toolchains, and what OpenAI's March 2026 acquisition of Astral means for Ruff's open-source future.

Key Takeaways

  • Ruff replaces Flake8, Black, isort, pyupgrade, pydocstyle, and autoflake with one tool and one pyproject.toml block
  • Run ruff check --fix before ruff format: reversing the order can cause linter autofixes to conflict with formatting changes
  • Ruff is NOT a type checker and does NOT fully replace Pylint's deep semantic analysis
  • Start with Tier 1 rules (E, W, F, I) and graduate to Tier 2 and Tier 3 as your codebase matures
  • The MIT license means the community can fork Ruff if OpenAI's acquisition shifts development priorities

What Is Ruff?

Ruff was created by Charlie Marsh in August 2022 as a side project to learn Rust. The original ambition was narrow: prove that Python tooling could run orders of magnitude faster by rewriting core primitives in a systems language.

The launch tweet put it plainly: "ruff is 10-100x faster than existing solutions. It lints the entire CPython codebase in < 500ms."

Three years and 416 releases later, Ruff is maintained by Astral and implements 900+ lint rules drawn from Flake8, isort, pyupgrade, bandit, refurb, pydocstyle, and more. A stable Black-compatible formatter graduated from preview in v0.3.0 (March 2024). The current stable version is v0.15.19 (June 2026).

Why Ruff Was Built

Python's linting ecosystem before Ruff fragmented across five or more tools, each with its own config file, CI install step, and version pin. Flake8 for style errors, Black for formatting, isort for import ordering, pyupgrade for syntax modernization, pydocstyle for docstring conventions. Every new project started by wiring five separate configs.

Marsh described the inspiration in The Changelog podcast: Rust's toolchain ships cargo clippy and rustfmt together, they cover linting and formatting in one command, and they run in milliseconds. Python had no equivalent. Ruff was built to close that gap.

OpenAI Acquires Astral (March 2026)

On March 19, 2026, OpenAI announced the acquisition of Astral. Charlie Marsh and the team will join OpenAI's Codex division; financial terms were not disclosed. The acquisition is pending regulatory approval.

Community reaction on r/Python was watchful rather than alarmed. Ruff, uv, and Astral's ty type checker are embedded deeply enough in Python that practitioners immediately looked up the license rather than the acquirer.

Both Ruff and uv are MIT-licensed. JetBrains' PyCharm team called the acquisition "a reflection of the impact they've had."

Charlie Marsh's statement: "Open source is at the heart of that impact and the heart of that story; it sits at the center of everything we do."

Simon Willison's framing is the most practical: the MIT license is the real guarantee, and it exists regardless of any company's stated intentions.

How Ruff Works: Linter vs. Formatter

Most tutorials blur this line. Getting it right shapes your CI pipeline and editor config.

Ruff the Linter (ruff check)

ruff check analyzes Python source code for style violations, potential bugs, security risks, and outdated patterns. Each violation surfaces as an error code: F401 for unused imports, E501 for lines over the length limit, B006 for mutable default arguments. Most rules include an auto-fix you apply with --fix.

Ruff the Formatter (ruff format)

ruff format reformats Python code to a consistent style: whitespace, quotes, trailing commas, and line wrapping. It is designed as a near-drop-in replacement for Black, producing >99.9% compatible output on real codebases. It does not catch bugs or style violations.

The Correct Command Order

This is the single most common beginner mistake, and Corey Schafer addresses it directly in his tutorial:

"You're generally going to want to run ruff check --fix first to fix the linting issues and then run ruff format afterwards. That way the fixes don't mess up any of the formatting."

Run the formatter first and you risk the linter's autofixes undoing some of its output on the next pass. The correct sequence:

Shell
ruff check --fix .   # 1. lint and autofix first
ruff format .        # 2. format after fixes are applied

Set this order in your pre-commit hooks and VS Code settings. You do not want to rely on remembering it at the command line.

What Ruff Is NOT

Ruff is not a type checker. It does not catch type mismatches: passing a str where an int is expected produces no Ruff violation.

Corey Schafer puts it directly: "I don't want you to confuse that with a type checker. A type checker analyzes your code and lets you know if you're actually using the wrong data types in different places."

Type checking is a separate job. It belongs to mypy, pyright, or Astral's own ty (still in development as of June 2026). Run them alongside Ruff, not instead of it.

Installing Ruff

Five installation paths, depending on your project setup:

Method

Command

pip

pip install ruff

uv (recommended for new projects)

uv add --dev ruff

pipx

pipx install ruff

Standalone binary

`curl -LsSf https://astral.sh/ruff/install.sh \

Homebrew (macOS/Linux)

brew install ruff

The uv pairing is the community default in 2026. On r/Python, uv + Ruff are treated as the new baseline stack: uv replaces pip and venv, Ruff replaces the full linting and formatting toolchain.

Starting a new Python project? Pair uv and Ruff from day one. For Python beginners, pip install ruff is the simplest entry point.

Windows note: Some r/learnpython users report PATH issues with Ruff binaries outside standard system paths. If ruff is not found after install, add the pip scripts directory to your PATH or run python -m ruff instead.

Configuring Ruff in pyproject.toml

Ruff reads configuration from pyproject.toml, ruff.toml, or .ruff.toml. A minimal setup that covers most projects:

Text
[tool.ruff]
line-length = 88
target-version = "py39"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
ignore = ["E501"]  # let the formatter handle line length

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]      # allow assert in tests
"__init__.py" = ["F401"]   # allow unused imports in __init__

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true

Tiered Rule Selection: A Maturity Path

Most guides either show minimal defaults or a full production config with no guidance on how to get there. Here's a graduated approach based on project maturity:

Tier 1: Every project from day one:

Text
select = ["E", "W", "F", "I"]

PEP 8 errors and warnings, Pyflakes (unused imports, undefined names), and import sorting. Low noise, immediate payoff, no surprises on existing codebases.

Tier 2: New projects and greenfield work:

Text
select = ["E", "W", "F", "I", "B", "C4", "UP", "SIM", "RUF"]

Adds flake8-bugbear (bug-prone patterns), comprehension simplifications, pyupgrade (modernize syntax), code simplifications, and Ruff-native rules. Expect more violations on a legacy codebase; apply these to new code first.

Tier 3: Mature teams with stable codebases:

Text
select = ["E", "W", "F", "I", "B", "C4", "UP", "SIM", "RUF", "S", "PL", "TCH"]

Adds bandit security checks, ported Pylint rules, and type-checking import helpers. Run ruff check --fix --unsafe-fixes . once to baseline a legacy codebase before enabling Tier 3.

Rule Categories Reference

Prefix

Source tool

What it catches

E / W

pycodestyle

PEP 8 style errors and warnings

F

Pyflakes

Unused imports (F401), undefined names (F821), unused variables (F841)

I

isort

Import ordering and grouping

UP

pyupgrade

Old-style string formatting, deprecated patterns

B

flake8-bugbear

Bug-prone patterns, mutable defaults, opinionated improvements

S

bandit

Security checks: SQL injection risks, hardcoded secrets

SIM

flake8-simplify

Simplifiable if-else chains, redundant code

PL

Pylint

Pylint rules ported to Ruff

RUF

Ruff-native

Ruff's own built-in rules (ambiguous variable names, implicit namespace packages)

D

pydocstyle

Docstring format and completeness

Default-enabled: a subset of F rules plus the E rules that don't conflict with the formatter. Everything else is opt-in.

Why Ruff Is So Fast (It Is Not Just Rust)

The "written in Rust" explanation is both true and incomplete. Speed is architectural.

Ruff's parser was rewritten from a parser-generator approach to a handwritten recursive-descent parser. That change alone added 30–40% more speed on top of the Rust baseline. Charlie Marsh corrected the misread in The Changelog interview: the language choice is a prerequisite, not the whole explanation.

Beyond the parser: IO caching means Ruff skips unchanged files on repeated runs. On a large codebase you're editing incrementally, most runs touch a handful of files. Parallel file processing spreads the remaining work across CPU cores.

The practical effect is qualitative, not just quantitative. At sub-200ms, linting stops being something you run before commits. You run it on every keystroke.

On r/learnpython, practitioners describe this directly: when linting takes 30 seconds, you disable it locally; when it takes 200 milliseconds, you keep it running continuously.

Real-world benchmark (47,000 lines / 180 files, Stackademic, May 2026):

Tool

Time

vs. Ruff

Ruff

0.18s

baseline

Flake8

8.34s

46× slower

Pylint

47.21s

262× slower

Ruff vs. Flake8, Black, and Pylint

Ruff vs. Flake8

Ruff re-implements every core Flake8 rule with the same error codes for backward compatibility. Your existing # noqa: comments work without modification. The differences: Ruff is 46–200× faster, includes auto-fix (Flake8 has none), and reads from a single config block instead of three separate files.

The FastAPI team's migration in October 2023 is the highest-profile single example in the ecosystem:

So, @FastAPI now uses the Ruff formatter. 😎✨ Ruff alone is now replacing (for me): * flake8 * autoflake * isort * pyupgrade * black ...and Ruff is still crazy fast. 🚀 I keep intentionally adding broken code just to ensure it is indeed running. 🤪
Sebastián Ramírez · @tiangoloView on X

Ruff vs. Black

On the Django codebase (2,772 files), switching from Black to Ruff produced only 34 files with any diff at all. The Ruff formatter is >30× faster than Black and produces output that is >99.9% compatible on real projects.

Known edge cases include end-of-line comment placement and magic trailing comma handling. Both surface as one-time cosmetic diffs during migration, not ongoing divergence.

Ruff vs. Pylint

This is where most competitor articles get it wrong. Ruff does NOT fully replace Pylint.

Pylint's deep semantic analysis catches patterns outside Ruff's scope: inconsistent return types, undefined variables in complex conditional branches, and explicit None-path warnings. The Stackademic benchmark showed Pylint flagging inconsistent return types and None-path warnings that both Flake8 and Ruff missed.

Safety-critical and large mature codebases often run both tools. Ruff handles the 90% of common checks at high speed; Pylint covers the deeper semantic layer.

Tool comparison at a glance:

Tool

Language

Time (47k lines)

Rules

Formatter

Auto-fix

Ruff

Rust

0.18s

900+

✅ Yes

✅ Yes

Flake8

Python

8.34s

~300 + plugins

❌ No

❌ No

Black

Python

~3–20s on large codebases

N/A

✅ Yes

N/A

isort

Python

Slow

N/A

Sort only

✅ Yes

Pylint

Python

47.21s

~400

❌ No

Limited

VS Code and Editor Integration

Install the official Ruff extension (charliermarsh.ruff) from the VS Code Marketplace. Add this to your settings.json:

Text
{
  "[python]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "charliermarsh.ruff",
    "editor.codeActionsOnSave": {
      "source.organizeImports": "explicit"
    }
  }
}

This replaces Black, isort, and Pylint extensions with a single Ruff extension. Diagnostics appear inline with click-to-documentation for each error code. Corey Schafer's Ruff tutorial explicitly uninstalls the Black and isort extensions after setting up Ruff.

PyCharm: Configure via External Tools or File Watchers. Set the program to the ruff binary, arguments to check --fix $FilePath$, and trigger to "After saving file." Alternatively, use the pre-commit plugin for team-level enforcement.

Pre-commit and GitHub Actions Integration

Pre-commit

YAML
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.19
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

Pre-commit is the team boundary: it blocks unformatted or lint-failing code from entering the repo regardless of each developer's editor setup. One known friction point: if you use both ruff-pre-commit and uv, keep both pinned to the same Ruff version. Drift between uv.lock and the pre-commit pin generates version-mismatch CI failures that read as lint violations.

GitHub Actions

YAML
name: Lint
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/ruff-action@v1
        with:
          args: check .
      - uses: astral-sh/ruff-action@v1
        with:
          args: format --check .

For teams adopting Ruff in CI for the first time, a two-stage rollout reduces friction. Start with ruff check --no-fix (fail-only, no autofixes) so developers can review violations without being blocked.

Enable --fix once the team is comfortable with the active rule set. This follows the approach recommended in r/learnpython adoption threads.

You can also add --output-format=github to surface inline annotations on pull requests:

Shell
ruff check --output-format=github .

This is useful for Python automation pipelines where you want violations surfaced as PR comments, not just CI build failures.

Migrating from Flake8 + Black + isort

A migration on a 47k-line codebase typically takes 1-2 hours. Here is the sequence:

  1. Install Ruff: pip install ruff or uv add --dev ruff
  2. Port Black settings: Move line-length from [tool.black] to [tool.ruff] and [tool.ruff.format]
  3. Preview formatting diff: Run ruff format --diff . and review any deviations before writing
  4. Enable lint rules: Add select = ["E", "F", "I", "UP", "B"] in [tool.ruff.lint]
  5. Port existing exceptions: Translate # noqa: comments and .flake8 per-file-ignores into per-file-ignores
  6. Apply safe fixes: ruff check --fix . and commit the result
  7. Remove old tools: Uninstall Black, isort, Flake8; remove their config blocks
  8. Update CI: Replace black --check, isort --check, flake8 with ruff check . and ruff format --check .
  9. Update pre-commit: Replace old hooks with ruff-pre-commit hooks

Config simplification measured on a 47k-line codebase: from 3 separate config files (setup.cfg, the [tool.black] section, .flake8) down to a single [tool.ruff] block. The # noqa: suppression comments you already have work without modification.

Common Ruff Python Mistakes to Avoid

Running ruff format Before ruff check --fix

The formatter can undo some linter autofixes when applied first. Always lint-then-format: ruff check --fix . before ruff format .. Set this order in both your pre-commit hooks and VS Code on-save settings so you cannot accidentally reverse it.

Treating Ruff as a Type Checker

Ruff does not analyze types. Passing a str where an int is expected produces no Ruff violation.

Add mypy or pyright alongside Ruff. They run independently and the combination covers both style and type safety.

Starting with select = ["ALL"]

This enables every available rule, including hundreds that conflict with each other or with the formatter. The result is hundreds of violations on any real codebase, most of them false positives. Start with Tier 1 (E, W, F, I) and expand deliberately.

Skipping per-file-ignores

Test files need assert statements, and S101 will flag every one. `init.py files re-export names that look like unused imports (F401). Set these exceptions early or you will spend time adding # noqa` comments across the codebase.

Assuming Ruff Fully Replaces Pylint

Most guides make this claim. It is inaccurate. Pylint catches inconsistent return types, complex None-path warnings, and undefined variables in conditional branches that Ruff misses.

On safety-critical codebases, run both. Ruff handles speed-dependent checks; Pylint handles depth-dependent ones.

Frequently Asked Questions

Related Articles