}

Ruff Python Linter 2026: Replace flake8, isort, and black with One Tool

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 ruff or uv add ruff --dev

Check code: ruff check .

Format code: ruff format .

Config: Add a [tool.ruff] section to pyproject.toml — see the full example in the Configuration section below.

Migrate from flake8 + isort + black: Remove those packages, add Ruff, run ruff check --fix . and ruff 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:

ToolTimeNotes
Ruff0.27sAll enabled rules
flake829.4sDefault rules only
pylint112.3sWith default plugins
black (check)8.1sFormat check only
isort (check)4.3sImport check only
flake8 + isort + black~42sSequential 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:

PrefixSourceDescription
EpycodestylePEP 8 style errors (indentation, whitespace, etc.)
WpycodestylePEP 8 style warnings
FpyflakesUndefined names, unused imports, syntax errors
IisortImport ordering and formatting
Npep8-namingNaming conventions (classes, functions, variables)
UPpyupgradeSyntax modernisation (f-strings, | unions, etc.)
Bflake8-bugbearLikely bugs and design issues
C4flake8-comprehensionsUnnecessary comprehension patterns
SIMflake8-simplifyCode simplification suggestions
Sflake8-banditSecurity vulnerabilities
ANNflake8-annotationsMissing type annotations
DpydocstyleDocstring conventions
PTflake8-pytest-stylepytest-specific patterns
T20flake8-printprint statement detection
ERAeradicateCommented-out code detection
PLpylintPylint-equivalent checks
RUFRuffRuff-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:

  1. Open Extensions (Ctrl+Shift+X / Cmd+Shift+X)
  2. Search for "Ruff"
  3. 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

Leonardo Lazzaro

Software engineer and technical writer. 10+ years experience in DevOps, Python, and Linux systems.

More articles by Leonardo Lazzaro