Before Ruff, a well-linted Python project typically ran flake8, isort, pyupgrade, and several other single-purpose tools, each with its own configuration and its own noticeable runtime. Ruff, written in Rust, reimplements the rules from dozens of these tools in one binary, fast enough to run on every save.
1The Fragmented World Ruff Replaced
Before Ruff's rise, a thoroughly-linted Python project typically chained several genuinely separate tools together: flake8 (itself a wrapper combining pycodestyle for style and pyflakes for logical errors) for core linting, isort specifically for import ordering, pyupgrade for automatically modernizing syntax to take advantage of newer Python version features, and often several additional flake8 plugins (flake8-bugbear for common bug patterns, pep8-naming for naming conventions) each adding their own specific checks. Each of these was a separate package to install, a separate configuration section (or separate config file entirely) to maintain, and a separate step in both local development and CI ā genuine coordination overhead, and genuine, if individually modest, runtime cost per tool that accumulated noticeably across a real, larger codebase.
Ruff, written in Rust specifically for performance, set out to reimplement the *rules* from this entire fragmented ecosystem ā not just clone flake8's behavior, but genuinely re-derive and re-implement the specific checks that made each of these individual tools valuable ā inside one single, fast, unified binary. select = ["E", "F", "I", "UP", "B"] in one [tool.ruff.lint] configuration section enables pycodestyle/pyflakes-equivalent checks (E/F), isort-equivalent import sorting (I), pyupgrade-equivalent modernization suggestions (UP), and flake8-bugbear-equivalent bug-pattern detection (B) ā functionality that previously required five separate tool installations and configurations, now expressed as one line in one config section.
This consolidation mirrors exactly the same 'one file, one tool, replacing several fragmented ones' story pyproject.toml itself told relative to the old setup.py/setup.cfg/requirements.txt/pytest.ini era ā Ruff applied that same consolidating philosophy specifically to the linting and formatting layer of the Python tooling ecosystem.
# The old multi-tool setup:
# flake8 -- style and error checking
# isort -- import sorting
# pyupgrade -- modernizing syntax for newer Python versions
# pep8-naming, flake8-bugbear, ... -- additional plugins
# Each: separate config, separate install, separate CI stepFive previously-separate tools' rules, one config, one binary
2Speed as a Genuine Feature, Not Just a Benchmark Number
Ruff's Rust implementation produces a speed difference from its Python-implemented predecessors that isn't merely a nice-to-have benchmark statistic ā it's large enough (often reported as one to two orders of magnitude faster on real, sizable codebases) to genuinely change *how* linting fits into a development workflow. A linting step slow enough to take several seconds or longer creates real friction: developers start batching lint checks rather than running them constantly, or configure it to run only in CI rather than locally, or ā in the worst case ā start ignoring or ostensibly disabling it because the interruption to their flow outweighs the immediate value of the feedback.
A linting step fast enough to complete in a small fraction of a second removes that friction entirely, making 'run on every file save' (a common editor integration setting) genuinely viable rather than merely theoretically nice ā the feedback loop between writing code and seeing lint/formatting issues shrinks from 'whenever I remember to run it, or whenever CI eventually runs it' to 'essentially immediately, every time I save.' This is precisely the same psychological and workflow effect the uv lesson identified for dependency resolution speed ā an operation fast enough to feel instantaneous gets used far more readily and far more often than one requiring a deliberate pause.
This speed advantage is also why Ruff was adopted so rapidly and broadly across the Python ecosystem in a relatively short period ā the value proposition (same or better rule coverage, one consolidated tool, dramatically faster) was compelling enough that migration effort was, for most projects, clearly worth it relative to the ongoing friction of the older, fragmented, slower toolchain.
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"] # pycodestyle, pyflakes, isort, pyupgrade, bugbear -- all in ONE tool
$ ruff check . # runs ALL selected rule categories at onceRemoves the friction that made linting easy to skip or defer
3ruff format and --fix: Beyond Just Reporting Problems
ruff format provides Black-compatible code formatting (Black, an earlier dedicated formatting tool, established many of the formatting conventions Ruff's formatter deliberately follows for compatibility) ā automatically reformatting code to a consistent style, eliminating an entire category of code-review discussion about spacing, line breaks, and quote style that a deterministic formatter settles automatically rather than through human debate.
ruff check --fix goes beyond formatting into automatically *correcting* many lint findings directly, not merely reporting them ā an unused import, an outdated syntax pattern pyupgrade's rules would flag, several other well-defined, unambiguous fixes are applied directly to your source files when you pass --fix, turning what would otherwise be a manual 'go fix each of these reported issues yourself' step into an automated correction, with the remaining, more genuinely judgment-requiring findings still reported normally for manual review.
This combination ā format automatically, auto-fix what's safely fixable, and clearly report the rest ā is what makes Ruff genuinely practical as a pre-commit hook or an editor's save-triggered action (the Pre-commit lesson later in this section covers exactly this integration pattern): most routine style and simple correctness issues are resolved automatically and silently, and only genuinely judgment-requiring findings interrupt the developer's actual attention, keeping the tool's value high relative to its interruption cost.
$ ruff format . # formats code (Black-compatible style)
$ ruff check --fix . # automatically fixes many lint findings, not just lists them
# Both run in a fraction of a second on most real projects --
# fast enough for an editor's 'format/fix on save' settingMost issues resolved automatically ā only genuine judgment calls surface
4Step-by-Step Breakdown
A linting step slow enough that developers start skipping it defeats its own purpose. Ruff's speed is fast enough that skipping it stops being tempting at all.
Before Ruff, a typical setup chained SEVERAL separate tools -- each with its own config file, its own installation, its own runtime cost.
ruff check reimplements the RULES from dozens of these tools in one Rust binary -- one install, one config section, one command.
Checkpoint: What does select = ["E", "F", "I", "UP", "B"] in [tool.ruff.lint] actually configure?
- āWhich categories of rules to enable (pycodestyle, pyflakes, isort, pyupgrade, bugbear) -- functionality that previously required several separate tools
- āA setting specific to just ONE underlying check, similar to a single flake8 flag
ruff format replaces Black -- and ruff check --fix AUTOMATICALLY fixes many findings, not just reports them.
Checkpoint: What is the key difference between ruff check and ruff check --fix?
- ā--fix automatically applies fixes for many findings; plain check only reports them without modifying code
- āThey check entirely different sets of rules
Ruff handles linting and formatting speed; MyPy is the next tool, for the deeper, type-level correctness checking linting alone can't provide.
Check Real Import Sort Order. Finish is_sorted_imports(): Ruff's import-sorting rule checks exactly this.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Configure Ruff to run on every file save in your editor, given its speed makes this genuinely practical
Ruff's speed removes the traditional friction of frequent linting ā configuring it for immediate, on-save feedback is a realistic, valuable workflow improvement that slower tools couldn't practically support.
Use ruff check --fix and ruff format to auto-resolve routine findings, reserving manual review for genuine judgment calls
This keeps the tool's interruption cost proportional to its value ā most style and simple correctness issues resolve automatically, while only findings genuinely requiring human judgment surface for review.
Frequent Bugs
Continuing to maintain a fragmented, older multi-tool linting setup (separate flake8, isort, pyupgrade configurations) out of inertia, missing Ruff's consolidation and speed benefits for an existing, already-configured project.
Migrate to Ruff's unified [tool.ruff] configuration, consolidating equivalent rule categories (select = [...]) from the previously separate tools into one config section and one CI step.
Real-World Examples
Migrating a Project From flake8 + isort + pyupgrade to Ruff
A team wants to consolidate their existing multi-tool linting setup into Ruff, reducing both configuration complexity and CI runtime.
# Before: separate configs for flake8 (setup.cfg), isort (.isort.cfg), pyupgrade (CLI flags in CI)
# After: one consolidated pyproject.toml section
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "C4"]
ignore = ["E501"] # line length handled by the formatter instead
# CI step: just `ruff check .` and `ruff format --check .`