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.

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.

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.
pyproject.toml blockruff check --fix before ruff format: reversing the order can cause linter autofixes to conflict with formatting changesE, W, F, I) and graduate to Tier 2 and Tier 3 as your codebase maturesRuff 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).
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.
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.
Most tutorials blur this line. Getting it right shapes your CI pipeline and editor config.
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 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.
This is the single most common beginner mistake, and Corey Schafer addresses it directly in his tutorial:
"You're generally going to want to runruff check --fixfirst to fix the linting issues and then runruff formatafterwards. 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:
ruff check --fix . # 1. lint and autofix first
ruff format . # 2. format after fixes are appliedSet this order in your pre-commit hooks and VS Code settings. You do not want to rely on remembering it at the command line.
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.
Five installation paths, depending on your project setup:
Method | Command |
|---|---|
pip |
|
uv (recommended for new projects) |
|
pipx |
|
Standalone binary | `curl -LsSf https://astral.sh/ruff/install.sh \ |
Homebrew (macOS/Linux) |
|
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.
Ruff reads configuration from pyproject.toml, ruff.toml, or .ruff.toml. A minimal setup that covers most projects:
[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 = trueMost 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:
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:
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:
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.
Prefix | Source tool | What it catches |
|---|---|---|
E / W | pycodestyle | PEP 8 style errors and warnings |
F | Pyflakes | Unused imports ( |
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 |
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.
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 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. 🤪
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.
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 |
Install the official Ruff extension (charliermarsh.ruff) from the VS Code Marketplace. Add this to your settings.json:
{
"[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.
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.19
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatPre-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.
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:
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.
A migration on a 47k-line codebase typically takes 1-2 hours. Here is the sequence:
pip install ruff or uv add --dev ruffline-length from [tool.black] to [tool.ruff] and [tool.ruff.format]ruff format --diff . and review any deviations before writingselect = ["E", "F", "I", "UP", "B"] in [tool.ruff.lint]# noqa: comments and .flake8 per-file-ignores into per-file-ignoresruff check --fix . and commit the resultblack --check, isort --check, flake8 with ruff check . and ruff format --check .ruff-pre-commit hooksConfig 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.
ruff format Before ruff check --fixThe 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.
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.
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.
per-file-ignoresTest 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.
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.

Learn Polars Python with real code examples and 2026 benchmarks. Filter 14 GB Parquet 11x faster than pandas using lazy evaluation.

Dagster's Software-Defined Assets model data pipelines as graphs of assets, not sequences of tasks. This guide covers the 2026 quickstart toolchain, dbt integration, Airflow comparison, and pricing.

FastAPI now outdownloads Flask 2.4× monthly — but async isn't always faster. A practitioner-level comparison of performance, validation, docs, security, and when to actually switch.