Introduction
Ruff is a Python linter and formatter written in Rust. In 2026 it has become the de facto standard for Python code quality tooling, used by FastAPI, Pydantic, Airflow, Pandas, and thousands of other projects. The reason is simple: it is 100x faster than flake8 on large codebases and replaces an entire suite of tools with a single binary.
Before Ruff, a typical Python project's requirements-dev.txt included flake8, black, isort, flake8-bugbear, flake8-comprehensions, pyupgrade, bandit, and a handful of glue plugins to keep them from conflicting with each other. Each tool had its own config file, its own CI step, and its own upgrade cycle. Ruff replaces all of them:
- flake8 — style and error checks (
E,W,Frules) - black — code formatting (
ruff format) - isort — import ordering (
Irules) - bandit — security checks (
Srules) - pyupgrade — syntax modernisation (
UPrules) - flake8-bugbear, flake8-comprehensions, pep8-naming, and 50+ other plugins
One tool. One config section in pyproject.toml. One binary to install.
Install Ruff
# with pip
pip install ruff
# with uv (recommended)
uv add --dev ruff
# or globally with uv tool
uv tool install ruff
Verify the installation:
ruff --version
# ruff 0.11.x
Basic Usage
# Lint your code
ruff check .
# Auto-fix fixable issues
ruff check --fix .
# Format code (like black)
ruff format .
# Check + format together
ruff check --fix . && ruff format .
A typical ruff check . run looks like this:
src/app/views.py:12:1: F401 [*] `os` imported but unused
src/app/views.py:34:5: B006 [*] Do not use mutable data structures for argument defaults
src/app/models.py:5:1: I001 [*] Import block is unsorted and/or unformatted
Found 3 errors.
[*] 3 fixable with the `--fix` option.
Lines marked [*] are auto-fixable. Pass --fix to correct them in place.
To see what --fix would change without writing anything:
ruff check --diff .
pyproject.toml Configuration
Ruff reads all configuration from pyproject.toml. Here is a production-ready starting point:
[tool.ruff]
line-length = 88
target-version = "py311"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"S", # bandit (security)
"N", # pep8-naming
]
ignore = ["E501"] # line too long (handled by formatter)
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
target-version tells Ruff which Python syntax is available, so pyupgrade rules (UP) know which modernisations to apply. Set it to match your project's minimum supported Python version.
ignore = ["E501"] is recommended when using ruff format alongside ruff check. The formatter already enforces line-length, so flagging the same rule twice creates noise.
Replacing flake8
ruff check implements every pycodestyle and pyflakes rule that flake8 covers by default, plus hundreds of rules from popular plugins. To start with an equivalent ruleset:
ruff check --select E,W,F .
To include the most commonly used flake8 plugins at the same time, expand select in pyproject.toml:
[tool.ruff.lint]
select = [
"E", "W", "F", # core flake8
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"N", # pep8-naming
]
No additional packages are required. Everything is built into Ruff.
To suppress a rule on a single line, use a standard # noqa comment:
import os # noqa: F401
Replacing black (Formatting)
# Instead of: black .
ruff format .
# Check without modifying (CI mode)
ruff format --check .
Ruff's formatter is intentionally black-compatible. For the vast majority of codebases it produces byte-for-byte identical output. If you are migrating from black, run ruff format . once, review the diff with git diff, and commit. From that point on ruff format . is the only formatting command you need.
To see what the formatter would change without writing files:
ruff format --diff .
Replacing isort
Add "I" to select in pyproject.toml:
[tool.ruff.lint]
select = ["I"]
[tool.ruff.lint.isort]
known-first-party = ["mypackage"]
Then run:
ruff check --fix .
Ruff sorts imports according to the same conventions as isort, and the [tool.ruff.lint.isort] section accepts the most common isort options translated to kebab-case. Common mappings:
| isort option | Ruff equivalent |
|---|---|
known_first_party | known-first-party |
force_sort_within_sections | force-sort-within-sections |
split_on_trailing_comma | split-on-trailing-comma |
Pre-commit Integration
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.0 # use latest
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
Install the hooks:
pip install pre-commit
pre-commit install
On every git commit, the ruff hook runs the linter and applies auto-fixes, and the ruff-format hook reformats any changed files. If either hook modifies files, the commit is aborted so you can review and re-stage the changes.
To run the hooks manually against all files (useful for the initial setup):
pre-commit run --all-files
Pin rev to a specific version tag. Use pre-commit autoupdate to bump it when you want to upgrade.
VS Code Integration
Install the "Ruff" extension from the VS Code marketplace. The publisher is astral-sh and the extension ID is charliermarsh.ruff.
Add the following to .vscode/settings.json:
{
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
}
}
This configures VS Code to format on every save, apply all auto-fixable lint corrections on save, and sort imports on save. Disable the old Python linting and formatting settings to avoid conflicts:
{
"python.formatting.provider": "none",
"python.linting.flake8Enabled": false,
"python.linting.pylintEnabled": false
}
GitHub Actions CI
- name: Lint with Ruff
uses: astral-sh/ruff-action@v3
with:
args: "check --output-format=github"
The --output-format=github flag emits annotations directly in the GitHub pull request diff view, so reviewers see linting errors inline alongside the changed code.
A complete workflow:
name: Lint
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint with Ruff
uses: astral-sh/ruff-action@v3
with:
args: "check --output-format=github"
- name: Format check
run: |
pip install ruff
ruff format --check .
Migrating an Existing Project
Follow these steps to migrate a project that currently uses flake8, black, and isort:
Step 1 — Audit current violations. Before changing configuration, see what Ruff would flag across all rules:
ruff check . --select ALL 2>&1 | head -50
This gives you an idea of scope without breaking anything.
Step 2 — Add pyproject.toml configuration. Start with the rules your project already follows, then expand from there. Copy the [tool.ruff] example from the Configuration section above.
Step 3 — Apply auto-fixes.
ruff check --fix .
This resolves unused imports, unsorted imports, and many code style issues automatically.
Step 4 — Fix remaining issues manually. Run ruff check . without --fix to see what is left. These are issues that require a human decision — logic changes, naming convention violations, or security findings.
Step 5 — Apply formatting.
ruff format .
Review the diff with git diff. The output should be nearly identical to black.
Step 6 — Remove old tools. Once the codebase is clean, remove flake8, black, isort, and their plugins from your dev dependencies:
pip uninstall flake8 black isort flake8-bugbear flake8-comprehensions pyupgrade bandit
Delete .flake8, .isort.cfg, and any [tool.black] or [tool.isort] sections from pyproject.toml.
Ruff vs Alternatives (2026)
| Tool | Speed | Replaces | Config file |
|---|---|---|---|
| Ruff | 100x faster | flake8 + black + isort + bandit | pyproject.toml |
| flake8 | Slow | linting only | .flake8 |
| black | Medium | formatting only | pyproject.toml |
| pylint | Very slow | linting (deep) | .pylintrc |
Ruff does not replace mypy or pyright. Those tools perform type inference, which is a different category of analysis that requires understanding the full type system. Ruff and a type checker are complementary — run both.
For most projects the migration is complete in under 30 minutes, the CI pipeline gets faster, the pre-commit hook becomes fast enough to be unnoticeable, and the development dependencies list shrinks significantly.