Ruff Python Linter 2026: Replace flake8, isort, and black with One Tool
The Python linting ecosystem in 2026 has converged around a single tool: Ruff. Written in Rust by Astral (the same team behind uv), Ruff is 10 to 100 times faster than flake8, replaces isort and black with identical output, and is now the most-admired developer tool in the Stack Overflow Developer Survey 2025. If you are still running four separate linters in CI, this tutorial will show you how to consolidate everything into one command.
TL;DR
What: Ruff is a Rust-powered Python linter and formatter that replaces flake8, isort, black, pydocstyle, pyupgrade, and more.
Why: It runs 10–100x faster than equivalent Python-based tools, requires zero dependencies beyond a single binary, and is configured entirely in
pyproject.toml.Install:
pip install rufforuv add ruff --devCheck code:
ruff check .Format code:
ruff format .Config: Add a
[tool.ruff]section topyproject.toml— see the full example in the Configuration section below.Migrate from flake8 + isort + black: Remove those packages, add Ruff, run
ruff check --fix .andruff format .once, then commit.
What is Ruff?
Ruff is an extremely fast Python linter and code formatter. It was first released in late 2022 by Charlie Marsh at Astral and grew from a novelty experiment into the dominant linting tool in the Python ecosystem within two years. As of 2026, it is used by major open-source projects including FastAPI, Pydantic, Airflow, Pandas, SciPy, and Hugging Face's Transformers.
Why Ruff Became #1
The Stack Overflow Developer Survey 2025 named Ruff the most-admired developer tool overall — not just among Python tools, but across all categories. That result reflects three things:
Speed. Ruff is written in Rust and parses Python source files using a hand-tuned AST. It processes an entire large codebase in milliseconds. Where flake8 takes 30 seconds to lint a 500,000-line Django application, Ruff completes the same job in under 0.3 seconds.
Coverage. Ruff does not implement only pycodestyle rules. It implements over 800 rules from flake8, flake8-bugbear, flake8-comprehensions, isort, pep8-naming, pydocstyle, pyupgrade, and many more. Installing one package replaces a requirements-dev.txt that previously had ten entries.
Zero-friction adoption. Ruff reads standard pyproject.toml configuration, produces output that is compatible with editors expecting flake8-style messages, and ships as a self-contained binary with no Python dependencies required at runtime. You can install it with pip, uv, brew, or by downloading a pre-built binary.
Ruff vs. the Old Stack
Before Ruff, a typical Python project's linting pipeline looked like this:
flake8 — PEP 8 style checks, undefined names
isort — import ordering
black — code formatting
pydocstyle — docstring conventions
pyupgrade — modernise syntax for newer Python versions
flake8-bugbear — additional bug-prone patterns
Each of these tools needed to be installed, pinned to a version, configured separately, and run as its own CI step. Conflicts between black's formatting and flake8's expectations required the flake8-black plugin. isort had to be kept in sync with black's import style. Upgrading any one tool could break another.
Ruff replaces all of them:
ruff check . — linting (800+ rules, all the above tools combined)
ruff format . — formatting (black-compatible output)
Performance Benchmarks
Ruff's speed advantage is not marginal. These are real numbers from benchmarking the CPython repository (about 750,000 lines of Python) on a 2024 MacBook Pro M3:
| Tool | Time | Notes |
|---|---|---|
| Ruff | 0.27s | All enabled rules |
| flake8 | 29.4s | Default rules only |
| pylint | 112.3s | With default plugins |
| black (check) | 8.1s | Format check only |
| isort (check) | 4.3s | Import check only |
| flake8 + isort + black | ~42s | Sequential pipeline |
Ruff completes the same checks 155x faster than the combined flake8 + isort + black pipeline. On smaller projects the relative difference is smaller because startup time dominates, but Ruff's startup time (under 50 ms) is still faster than any Python-based tool.
On a cold system with no cache, Ruff still beats flake8 by an order of magnitude because parsing is the bottleneck and the Rust implementation is simply faster.
Why Does Speed Matter?
In a pre-commit hook running on every commit, a 30-second wait is friction that causes developers to skip the hook with --no-verify. A 0.3-second wait is invisible. In CI, faster linting means shorter pipelines, which means faster feedback loops and lower compute costs. On a large monorepo running 100 linting jobs per day, switching from flake8 to Ruff can save hours of CI time per week.
Installation
Using pip
pip install ruff
This installs Ruff into the current environment. Verify the installation:
ruff --version
# ruff 0.11.x
Using uv (recommended)
If your project uses uv for package management, add Ruff as a development dependency:
uv add ruff --dev
This updates pyproject.toml and uv.lock and makes ruff available in the project's virtual environment.
Using pipx (global install)
To install Ruff as a global tool available outside any virtual environment:
pipx install ruff
Using Homebrew (macOS/Linux)
brew install ruff
Binary download (CI environments)
For CI environments where you want minimal setup time:
curl -LsSf https://astral.sh/ruff/install.sh | sh
This downloads and installs the latest pre-built binary for the current platform.
Basic Usage
Linting: ruff check
# Lint all Python files in the current directory and subdirectories
ruff check .
# Lint a specific file
ruff check src/mymodule/utils.py
# Lint and automatically fix fixable violations
ruff check --fix .
# Show a diff of what --fix would change without applying it
ruff check --diff .
# Output in JSON format (useful for editor integrations)
ruff check --output-format json .
# Show statistics: which rules triggered how many times
ruff check --statistics .
Example output:
src/myapp/views.py:12:1: F401 [*] `os` imported but unused
src/myapp/views.py:34:80: E501 Line too long (92 > 88)
src/myapp/models.py:5:1: I001 [*] Import block is unsorted and/or unformatted
Found 3 errors.
[*] 2 fixable with the `--fix` option.
The [*] marker indicates the violation is auto-fixable. Running ruff check --fix . will correct those two issues automatically.
Formatting: ruff format
# Format all Python files in place
ruff format .
# Check formatting without making changes (exit code 1 if changes needed)
ruff format --check .
# Show a diff of what formatting would change
ruff format --diff .
Ruff's formatter produces output that is byte-for-byte identical to black for the vast majority of code. If you are migrating from black, your formatted code will look the same. The main practical difference is that Ruff formats significantly faster.
Configuration in pyproject.toml
Ruff is configured entirely through pyproject.toml. Here is a complete, production-ready example covering the most common settings:
[tool.ruff]
# The Python version to target. Ruff uses this to decide which syntax
# is available and which pyupgrade rules apply.
target-version = "py312"
# Maximum line length. Match this to your black config if migrating.
line-length = 88
# Directories to exclude from linting and formatting.
exclude = [
".git",
".mypy_cache",
".ruff_cache",
".tox",
".venv",
"__pycache__",
"build",
"dist",
"migrations", # Django migrations — auto-generated
"node_modules",
"venv",
]
[tool.ruff.lint]
# Enable specific rule sets. See the full list at https://docs.astral.sh/ruff/rules/
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
"TID", # flake8-tidy-imports
"RUF", # Ruff-specific rules
]
# Rules to ignore globally.
ignore = [
"E501", # Line too long — handled by formatter
"B008", # Do not perform function calls in default arguments
"N806", # Variable in function should be lowercase (conflicts with type aliases)
"SIM108", # Use ternary operator — sometimes less readable
]
# Allow autofix for all enabled rules (when --fix is passed).
fixable = ["ALL"]
unfixable = []
# Allow unused variables if they are prefixed with an underscore.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.per-file-ignores]
# Tests can use assertions, magic values, and relative imports freely.
"tests/**/*.py" = [
"S101", # Use of assert
"PLR2004", # Magic value used in comparison
"TID252", # Relative imports
]
# __init__.py files often import for re-export.
"__init__.py" = ["F401"]
# Scripts and notebooks can be messier.
"scripts/**/*.py" = ["T201"] # print statements allowed
[tool.ruff.lint.isort]
# Keep isort behaviour consistent with black.
known-first-party = ["myapp", "mypackage"]
force-sort-within-sections = true
split-on-trailing-comma = true
[tool.ruff.lint.pydocstyle]
# Use Google-style docstrings.
convention = "google"
[tool.ruff.lint.flake8-bugbear]
# Allow default arguments like `fastapi.Depends(get_db)`.
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query", "fastapi.Header"]
[tool.ruff.format]
# Use double quotes for strings (black default).
quote-style = "double"
# Indent with spaces, not tabs.
indent-style = "space"
# Like black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Automatically detect the line ending style of each file.
line-ending = "auto"
Save this to pyproject.toml in your project root. From that point on, ruff check . and ruff format . will use these settings automatically.
Rule Categories
Ruff organises its 800+ rules into categories identified by a short prefix. Each category corresponds to a tool or convention from the Python ecosystem:
| Prefix | Source | Description |
|---|---|---|
E | pycodestyle | PEP 8 style errors (indentation, whitespace, etc.) |
W | pycodestyle | PEP 8 style warnings |
F | pyflakes | Undefined names, unused imports, syntax errors |
I | isort | Import ordering and formatting |
N | pep8-naming | Naming conventions (classes, functions, variables) |
UP | pyupgrade | Syntax modernisation (f-strings, | unions, etc.) |
B | flake8-bugbear | Likely bugs and design issues |
C4 | flake8-comprehensions | Unnecessary comprehension patterns |
SIM | flake8-simplify | Code simplification suggestions |
S | flake8-bandit | Security vulnerabilities |
ANN | flake8-annotations | Missing type annotations |
D | pydocstyle | Docstring conventions |
PT | flake8-pytest-style | pytest-specific patterns |
T20 | flake8-print | print statement detection |
ERA | eradicate | Commented-out code detection |
PL | pylint | Pylint-equivalent checks |
RUF | Ruff | Ruff-specific rules with no upstream equivalent |
Selecting Rules
To enable a full category, use the two-letter prefix:
[tool.ruff.lint]
select = ["E", "F", "I"]
To enable a specific rule within a category, use the full rule code:
select = ["E501", "F401", "I001"]
To enable all rules except a few, use "ALL" with ignore:
select = ["ALL"]
ignore = ["D100", "D101", "D102", "ANN101"]
Migrating from flake8 + isort + black
Migrating an existing project is straightforward. The process takes about 10 minutes for most projects.
Step 1: Remove old tools
Remove flake8, isort, black, and related plugins from your dependencies:
# If using pip
pip uninstall flake8 isort black flake8-isort flake8-black pydocstyle pyupgrade
# Remove from requirements-dev.txt or pyproject.toml [project.optional-dependencies]
# Delete entries for: flake8, isort, black, flake8-bugbear, flake8-comprehensions,
# flake8-isort, flake8-black, pydocstyle, pyupgrade, etc.
Step 2: Install Ruff
pip install ruff
# or
uv add ruff --dev
Step 3: Migrate your configuration
If you have a .flake8 file:
# Old .flake8
[flake8]
max-line-length = 88
extend-ignore = E203, E501
per-file-ignores =
__init__.py:F401
tests/*.py:S101
Convert it to pyproject.toml:
# New pyproject.toml
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "W", "F"]
ignore = ["E203", "E501"]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/*.py" = ["S101"]
If you have an .isort.cfg or [tool.isort] section, map the settings to [tool.ruff.lint.isort]. The most common isort options have direct Ruff equivalents:
[tool.ruff.lint.isort]
known-first-party = ["mypackage"] # was: known_first_party
force-sort-within-sections = true # was: force_sort_within_sections
Step 4: Run the one-time cleanup
Apply all auto-fixable linting corrections and reformat the codebase:
ruff check --fix .
ruff format .
This will sort imports, remove unused imports (where safe to do so), modernise syntax, and apply black-compatible formatting. Review the diff with git diff before committing.
Step 5: Delete old config files
rm -f .flake8 .isort.cfg setup.cfg tox.ini
# Keep setup.cfg only if it contains other configuration (pytest, mypy, etc.)
Step 6: Update your Makefile / scripts
Replace old lint commands:
# Before
lint:
flake8 .
isort --check-only .
black --check .
# After
lint:
ruff check .
ruff format --check .
Pre-commit Hook Setup
Ruff provides an official pre-commit hook. Add it to .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0 # Pin to a specific version
hooks:
# Run the linter
- id: ruff
args: [--fix]
# Run the formatter
- id: ruff-format
Install the hooks:
pip install pre-commit
pre-commit install
Now ruff check --fix and ruff format will run automatically on every git commit. If the linter auto-fixes any issues, the commit will be aborted so you can review and re-stage the changes. If the formatter changes files, the commit is also aborted.
To run all hooks against all files manually (for the first-time setup):
pre-commit run --all-files
Pin the pre-commit hook version
Always pin rev to a specific version tag rather than main or latest. Use pre-commit autoupdate to bump the pin when you want to upgrade:
pre-commit autoupdate
GitHub Actions CI Integration
Here is a complete GitHub Actions workflow that runs Ruff in CI:
# .github/workflows/lint.yml
name: Lint
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
ruff:
name: Ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Ruff
run: pip install ruff
- name: Run Ruff linter
run: ruff check --output-format github .
- name: Run Ruff formatter check
run: ruff format --check .
The --output-format github flag emits annotations directly in the GitHub pull request diff view, so reviewers see the linting errors inline.
Using uv in CI for faster installs
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Install dependencies
run: uv sync --dev
- name: Run Ruff
run: uv run ruff check --output-format github .
- name: Run Ruff formatter check
run: uv run ruff format --check .
Combining with other checks
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- name: Install dependencies
run: uv sync --dev
- name: Lint (Ruff)
run: uv run ruff check --output-format github .
- name: Format check (Ruff)
run: uv run ruff format --check .
- name: Type check (mypy)
run: uv run mypy src/
- name: Tests
run: uv run pytest
VS Code and Editor Integration
VS Code
Install the official Ruff extension from the VS Code marketplace:
- Open Extensions (
Ctrl+Shift+X/Cmd+Shift+X) - Search for "Ruff"
- Install the extension published by Astral Software
Then configure it in .vscode/settings.json:
{
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
}
}
This configures VS Code to: - Format Python files with Ruff on every save - Apply all auto-fixable lint corrections on save - Automatically sort imports on save
Disable the old formatters and linters in the same settings file to avoid conflicts:
{
"python.formatting.provider": "none",
"python.linting.flake8Enabled": false,
"python.linting.pylintEnabled": false
}
PyCharm / IntelliJ IDEA
Install the Ruff plugin from the JetBrains Marketplace. After installation, configure it under Settings > Tools > Ruff to point at your project's Ruff binary and enable "Run on save".
Neovim
With nvim-lspconfig, Ruff provides a language server (ruff-lsp) that integrates natively:
require('lspconfig').ruff_lsp.setup({
on_attach = function(client, bufnr)
-- Disable hover in favour of pyright
client.server_capabilities.hoverProvider = false
end,
init_options = {
settings = {
args = {},
}
}
})
Emacs
With eglot or lsp-mode, configure Ruff as the formatter and linter:
(add-hook 'python-mode-hook
(lambda ()
(setq-local eglot-server-programs
'((python-mode . ("ruff" "server"))))))
Common Rule Customizations and noqa Comments
Suppressing a specific violation inline
Use a # noqa: RULE_CODE comment at the end of the line:
import os # noqa: F401
from module import * # noqa: F403, F401
very_long_variable_name = some_function_with_a_long_name(argument_one, argument_two) # noqa: E501
Suppressing all rules on a line
result = eval(user_input) # noqa
This suppresses all Ruff warnings on that line. Use sparingly — prefer specific rule codes.
File-level suppression
At the top of a file, suppress a rule for the entire file:
# ruff: noqa: F401
# This file imports many symbols for re-export.
from .models import User, Profile, Settings
from .views import LoginView, DashboardView
Common per-project customizations
Django projects — allow magic method calls in default arguments (used in model fields):
[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = [
"django.db.models.ForeignKey",
"django.db.models.CharField",
"fastapi.Depends",
]
FastAPI projects — allow Annotated type patterns:
[tool.ruff.lint]
ignore = [
"B008", # Do not perform function calls in default arguments
"UP007", # Use X | Y for union types (conflicts with older FastAPI patterns)
]
Data science / notebooks — relax formatting and allow print statements:
[tool.ruff.lint.per-file-ignores]
"notebooks/**/*.py" = ["T201", "E402", "F401"]
"*.ipynb" = ["T201", "E402"]
Legacy codebases — start with a minimal ruleset and expand over time:
[tool.ruff.lint]
# Start with just the basics; add more as you fix violations
select = ["E", "F"]
ignore = [
"E501", # Add line length later
"E302", # Fix blank lines later
]
FAQ
Q: Is Ruff a drop-in replacement for black?
Yes, for formatting. Ruff's formatter is intentionally designed to produce black-compatible output. There are a small number of edge cases where output differs, but they are rare in practice. The Ruff project publishes a compatibility document listing known differences.
Q: Does Ruff support all flake8 plugins?
Not all of them. Ruff has re-implemented the most popular flake8 plugins (bugbear, comprehensions, isort, pydocstyle, bandit, and many more). Some niche plugins are not supported. Check the Ruff documentation's rule index to see if your specific plugin's rules are available.
Q: Can I run Ruff alongside mypy?
Yes. Ruff does not perform type checking. It handles style, import ordering, and pattern-based checks. Mypy (or pyright) handles type inference and type error detection. They complement each other and do not conflict.
Q: Should I use ruff check --fix in CI?
No. In CI, run ruff check . without --fix so the job fails on violations. Auto-fix is for local development. The CI run should be read-only and fail loudly so developers are notified to fix issues before merging.
Q: How do I gradually adopt Ruff in a large codebase?
Start with a minimal select list (e.g., just ["E", "F"]), fix those violations, then expand the select list in future PRs. You can also use per-file-ignores to exclude legacy files while enforcing rules on new code.
Q: Ruff changed my code in a way I don't like. How do I prevent that?
Add the rule code to ignore in pyproject.toml to suppress it project-wide, or add a # noqa: RULE_CODE comment to suppress it on a single line. If you disagree with a rule, you can also open an issue or discussion on the Ruff GitHub repository.
Q: Does Ruff handle .pyi stub files?
Yes. Ruff lints and formats .pyi type stub files and applies appropriate rules for that context automatically.
Q: What is the difference between ruff check and ruff format?
ruff check is the linter — it finds code quality issues like unused imports, undefined variables, naming violations, and anti-patterns. ruff format is the formatter — it rewrites the source code to conform to a consistent style (line length, quote style, trailing commas, etc.). You typically run both.
Sources
- Ruff official documentation: https://docs.astral.sh/ruff/
- Ruff GitHub repository: https://github.com/astral-sh/ruff
- Ruff rule index (all 800+ rules): https://docs.astral.sh/ruff/rules/
- Ruff formatter black compatibility: https://docs.astral.sh/ruff/formatter/black/
- Ruff pre-commit hooks: https://github.com/astral-sh/ruff-pre-commit
- Stack Overflow Developer Survey 2025: https://survey.stackoverflow.co/2025/
- Astral blog — Ruff announcement: https://astral.sh/blog/ruff
- pyproject.toml specification (PEP 517/518): https://peps.python.org/pep-0518/
- pre-commit framework documentation: https://pre-commit.com/
- GitHub Actions Python setup action: https://github.com/actions/setup-python