Home Python Python Ruff Catches 900 Rules — Lint Setup That Scales
Beginner 3 min · September 07, 2026
Ruff Python Linting and Formatting

Python Ruff Catches 900 Rules — Lint Setup That Scales

Flake8 plus Black plus isort is slow and split.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.

Follow
Production
production tested
September 22, 2026
last updated
1,799
articles · all by Naren
Before you start⏱ 20 min
  • Basic Python syntax knowledge
  • A Python project with pyproject.toml
  • Git plus pre-commit installed
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Ruff is Astral's Rust linter and formatter for Python — one binary replacing Flake8, Black, isort, pyupgrade, and autoflake
  • Two subcommands: ruff check finds violations across 900+ rules, ruff format enforces Black-compatible style; run both in pre-commit and CI
  • Performance insight: Ruff runs 10-100x faster than Flake8/Black with built-in caching — warm runs on unchanged files finish in milliseconds, not minutes
  • Production rule: roll out rule families incrementally (E,F first, then B, UP, I, SIM) and run CI as bare ruff check so violations fail instead of being silently rewritten
  • Set target-version to your real minimum Python or UP rules will suggest syntax production cannot parse
  • Biggest mistake: select ALL on day one plus --fix --unsafe-fixes in CI — that buries the team and can rewrite semantics nobody reviewed
✦ Definition~90s read
What is Ruff Python Linting and Formatting?

Ruff is an extremely fast Python linter and code formatter written in Rust, built by Astral. It replaces Flake8 (plus dozens of plugins), Black, isort, pydocstyle, pyupgrade, and autoflake with a single binary that executes tens to hundreds of times faster while covering 900+ built-in rules.

Picture an editor who reads your entire novel in three seconds and flags every typo, repeated phrase, and outdated expression — then fixes the safe ones for you.

Its architecture runs one parse per file through natively reimplemented rules (Flake8, bugbear, pyupgrade, isort logic ported to Rust) plus a Black-compatible formatter forked from Rome, with built-in caching that skips unchanged files. Drop-in parity with Flake8, isort, and Black plus pyproject.toml configuration makes migration mechanical.

The trade-off is breadth versus depth: Ruff covers the plugin ecosystem's greatest hits but not every exotic Flake8 plugin, and its preview rules can shift semantics between releases. Deep type-aware analysis still belongs to mypy or pyright — Ruff is the fast syntactic layer, not a type checker.

Plain-English First

Picture an editor who reads your entire novel in three seconds and flags every typo, repeated phrase, and outdated expression — then fixes the safe ones for you. That's Ruff for Python code. Before it, you needed four different editors (one for spelling, one for margins, one for chapter order, one for modern word usage), each slow and each with its own rulebook. Ruff merges them into a single speed-reader with 900+ checks built in. It learns your preferences from one short config file, ignores the chapters you tell it to skip, and finishes before you've lifted your fingers off the keyboard. The catch is the same as with any strict editor: turn on every rule on day one and you'll get 12,000 red marks and stop reading them.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Python linting used to mean assembling a toolchain. Flake8 for errors, Black for formatting, isort for imports, pyupgrade for modernization, plus a plugin for every extra opinion. Each tool parsed your code separately, so a pre-commit run took minutes and configs lived in three files.

Slow feedback kills adoption. When lint takes longer than the test suite, developers skip the hook with --no-verify. You'll recognize the result: PRs full of style nits and real bugs hiding behind them.

Ruff ends the assembly job. One Rust binary runs 900+ rules and a Black-compatible formatter in milliseconds. It reads a single config section and plugs into pre-commit, CI, and VS Code cleanly.

Speed alone won't save you though. Enabling every rule at once buries the team, and mixing two formatters creates phantom diffs. This guide shows the rollout that sticks.

Why Ruff Replaces Four Tools Instead of Joining Them

The classic Python toolchain grew by accretion. Flake8 checks errors, Black formats, isort sorts imports, pyupgrade modernizes syntax — each parses your code independently, each has its own config, and a pre-commit run pays the startup cost four times. On a large repo that means minutes per commit.

Ruff reimplements all of it in Rust behind one interface. Its 900+ rules are native ports of Flake8 plugins (bugbear, pyupgrade, isort, pydocstyle, and more), and its formatter is a Black-compatible fork of the Rome formatter. One parse feeds every check, and a global cache skips unchanged files.

The practical effect is a different development rhythm. Lint-on-save in VS Code answers instantly, pre-commit hooks finish before you context-switch, and CI lint stages stop dominating pipeline time. Speed is not vanity here — it is what makes the checks actually run.

📊 Production Insight
Teams that move lint from a minutes-long CI stage to a milliseconds-long pre-commit hook see hook-bypass rates collapse — fast checks get run, slow ones get skipped with --no-verify.
🎯 Key Takeaway
One Rust parse feeds 900+ native rules plus formatting, with caching that makes warm runs instant.

Install Ruff and Baseline Before Changing Anything

Install Ruff with pip, uv, or the standalone installer, then take a baseline before changing anything. ruff check --statistics ranks your current violations by rule so you know what you are adopting. ruff format --check shows how far your style is from the formatter without touching files.

Do not fix anything yet. The baseline tells you which rule families are cheap to enable (usually E and F) and which will need a campaign (often SIM or D). That ordering drives the whole rollout.

BASH
1
2
3
4
pip install "ruff==0.16.6"
ruff --version
ruff check --statistics .
ruff format --check .
📊 Production Insight
Skipping the baseline is how teams end up with 12,000 violations on Monday. Ten minutes of measurement saves a six-week cleanup.
🎯 Key Takeaway
Measure first with --statistics and format --check; the baseline decides your rollout order.

Configure Once — pyproject.toml That Survives Review

All Ruff configuration lives in one place: pyproject.toml (or ruff.toml). Set line-length and target-version first — target-version gates every modernization suggestion, so it must match your oldest production interpreter.

The lint section selects rule families: E (errors), F (pyflakes), B (bugbear), UP (pyupgrade), I (isort). Start with that set. Per-file ignores exempt tests from assert rules and migrations from everything. Keep unsafe-fixes off globally and enable it only for deliberate one-shot cleanups.

The format section mirrors Black options. One decision matters most: Ruff format or Black, never both. The example above is a sane production starting point for a Python 3.12 fleet.

pyproject.tomlTOML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[tool.ruff]
line-length = 88
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "B", "UP", "I"]
ignore = ["E501"]
fixable = ["ALL"]
unsafe-fixes = false

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]
"**/migrations/*" = ["ALL"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
⚠ Never run two formatters
Ruff format and Black disagree on edge cases. Running both guarantees phantom diffs on every PR. Delete one.
📊 Production Insight
The target-version line is the highest-value line in the file. Every UP suggestion is gated on it — get it wrong and Ruff either nags about impossible upgrades or suggests syntax that crashes production.
🎯 Key Takeaway
One config section: target-version matching production, five starter families, per-file ignores for tests and migrations.

check vs format vs fix — The Daily Commands

ruff check reports violations; --fix applies safe autofixes like removing unused imports. Unsafe fixes — things that can change semantics — require explicit opt-in and a human reading the diff. That two-tier design is the whole safety model.

Local workflow: ruff check --fix . then ruff format ., review the diff, commit. CI workflow: bare ruff check . and ruff format --check . with no fix flags, so violations fail the build. Fixing in CI means CI authors code nobody reviews.

--statistics stays useful forever: run it monthly to find the next rule family worth enabling. Each new family is a small PR with an owner, not a big bang.

📊 Production Insight
CI that autofixes once rewrote exception handling in a payments module and stayed green. Bare check in CI would have failed the PR before merge.
🎯 Key Takeaway
Developers fix locally with --fix; CI fails loudly without it; statistics guides the next family.

Pre-commit, VS Code, and CI Wired Together

Two hooks cover everything: ruff-check with --fix for lint autofixes, ruff-format for style. Pin rev to an exact version and bump it monthly with pre-commit autoupdate — unpinned hooks are how phantom diffs sneak in.

Pair the hooks with the first-party VS Code extension for instant feedback while typing. The extension uses the same binary and config, so editor and CI never disagree.

For GitHub Actions, the ruff-action runs the same checks on the whole repo. Keep the local and remote versions identical; version skew between hook and action produces works-here-fails-there confusion.

.pre-commit-config.yamlYAML
1
2
3
4
5
6
7
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.16.6
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format
📊 Production Insight
Version skew between pre-commit rev and CI action is a top source of green-local-red-remote. Pin both to the same tag and bump them together.
🎯 Key Takeaway
Two pinned hooks plus the VS Code extension plus the same version in CI — no skew anywhere.

Rolling Out to Legacy Code Without a Revolt

Legacy codebases need a campaign, not a flag day. Week one: E and F with autofix. Week two: B (bugbear catches real bugs). Week three: UP (modernize syntax). Then I (import sorting) and SIM (simplifications), each as a reviewed PR.

Preview rules stay off in production config — they are the unstable frontier where the incident above happened. Revisit them quarterly, never on a Friday.

Track the burn-down with --statistics in a visible place. Watching SIM violations drop from 800 to zero across four weeks keeps the team motivated in a way a single giant PR never does.

📊 Production Insight
The team in the incident recovered by reverting to E+F and adding one family per week. Six weeks later the full set was on with zero reverts — slower start, faster finish.
🎯 Key Takeaway
One family per week with owners and statistics; preview rules stay off until quarterly review.
● Production incidentPOST-MORTEMseverity: high

The Friday Linter Swap That Rewrote Exception Handling

Symptom
Monday's deploy raised uncaught exceptions in the payments retry path. The diff showed Ruff's autofix had touched exception handlers nobody had edited in months. CI was green because CI itself had applied the fix.
Assumption
The platform team assumed lint parity meant behavior parity: same rule codes, same outcomes, just faster. They enabled the full rule set including preview rules in one PR, expecting a handful of findings across a mature codebase.
Root cause
Two mistakes compounded. First, select = ["ALL"] on a 400k-line legacy codebase surfaced a decade of tolerated patterns at once. Second, a preview rule for exception-tuple parentheses (aimed at Python 3.14 semantics) rewrote except (ValueError,) handling in a payments module whose production interpreter was 3.11. The rewrite was syntactically valid but changed which exceptions were caught, and it shipped because CI ran --fix instead of failing.
Fix
They reverted to E and F rules only, then added one family per week (B, UP, I, SIM) with a reviewer per family. Preview rules were pinned off in production config. The pre-commit hook runs ruff check --fix plus ruff format; CI runs bare ruff check and ruff format --check to fail loudly. Six weeks later the full set was on with zero reverts.
Key lesson
  • Linter migrations are behavior changes disguised as tooling changes. Roll out rule families like feature flags: incrementally, with owners and a revert path.
  • CI should fail on violations, never silently fix them. Autofix in CI rewrites code nobody reviewed.
Production debug guideFour failure patterns behind most Ruff rollout incidents — with exact diagnostics.4 entries
Symptom · 01
Ruff ignores your config — rules you disabled still fire
Fix
Run ruff check --show-settings and ruff check --explain <CODE> to see which config file wins and what the rule means. Fix: consolidate to one pyproject.toml section and delete stale .flake8/setup.cfg files that shadow it.
Symptom · 02
Every PR shows formatting diffs nobody authored
Fix
Run ruff format --check --diff on one file to see the contested lines, then check pre-commit config for both black and ruff-format hooks. Fix: keep exactly one formatter and reformat the repo in a single isolated commit.
Symptom · 03
Thousands of violations appear after enabling a new rule family
Fix
Run ruff check --statistics to rank violations, then scope the noisy family with per-file ignores in pyproject.toml. Fix: enable families incrementally instead of ALL, and exempt generated files and migrations explicitly.
Symptom · 04
Ruff suggests syntax that crashes on the production Python version
Fix
Check target-version against your production interpreter (python3 --version on the deploy image). Fix: set target-version to the true minimum and re-run; UP suggestions that cannot parse there must stay disabled.
Ruff vs Classic Stack vs Pylint at a Glance
FeatureRuffFlake8 + Black + isortPylint
Speed10-100x faster (Rust), ms on warm cacheSeconds to minutes per runSlowest, deep analysis costs time
ScopeLint + format + sort in one binaryThree tools, three configsLint only, no formatter
Rules900+ built-in, Flake8 plugin portsNeeds a plugin per concernFewer, heavier checks
AutofixSafe --fix plus opt-in unsafe fixesPartial (autoflake, pyupgrade)Rarely autofixes
ConfigOne pyproject.toml sectionsetup.cfg, .flake8, pyproject splitHeavy .pylintrc
Editor storyFirst-party VS Code extensionCommunity plugins varyMature but slower feedback
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
pip install "ruff==0.16.6"Install Ruff and Baseline Before Changing Anything
pyproject.toml[tool.ruff]Configure Once
.pre-commit-config.yamlrepos:Pre-commit, VS Code, and CI Wired Together

Key takeaways

1
Ruff replaces Flake8, Black, and isort with one Rust binary running 900+ rules at 10-100x speed.
2
ruff check lints, ruff format styles
run both, and never pair Ruff format with Black.
3
Roll out rule families incrementally (E,F then B, UP, I, SIM) tracked by --statistics.
4
Safe --fix is routine; --unsafe-fixes needs human review and never runs in CI.
5
Set target-version to your real minimum Python so suggestions always parse in production.

Common mistakes to avoid

4 patterns
×

Enabling ALL rules on a legacy codebase at once

Symptom
12,000 violations appear overnight, the team mass-ignores everything, and the linter becomes wallpaper nobody reads.
Fix
Migrate in two passes: first ruff check --select E,F --fix for safe fixes, then enable one family at a time (B, UP, I, SIM) with team review. Track progress with ruff check --statistics.
×

Running Black and Ruff format together

Symptom
Every PR shows phantom diffs as the two formatters fight over line breaks. Developers stop trusting format checks entirely.
Fix
Pick one formatter — Ruff or Black — and delete the other from pre-commit and CI. If migrating, run ruff format once over the repo in a single isolated commit.
×

Leaving target-version unset while enabling pyupgrade rules

Symptom
Ruff suggests syntax your production interpreter rejects, or it stays silent about upgrades you could safely take. Either way the UP family misleads.
Fix
Set target-version to your actual minimum supported Python and let UP rules modernize syntax safely. Keep per-file ignores for genuine exceptions like intentionally dynamic imports.
×

Auto-fixing everything with --fix --unsafe-fixes in CI

Symptom
CI rewrites semantics it does not understand — an unused-variable fix deletes a side-effecting call. The pipeline is green and the app is broken.
Fix
Run ruff check --fix for safe autofixes and review unsafe ones individually. In CI, run ruff check without --fix so violations fail the build instead of being silently rewritten.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is Ruff and how does it replace the classic Python toolchain?
Q02SENIOR
Explain safe versus unsafe fixes in Ruff.
Q03SENIOR
How do you introduce Ruff to a large legacy codebase?
Q01 of 03SENIOR

What is Ruff and how does it replace the classic Python toolchain?

ANSWER
Ruff is Astral's Rust-based Python linter and formatter, 10-100x faster than Flake8 and Black, with 900+ built-in rules ported from across the ecosystem. ruff check finds violations, ruff format enforces Black-compatible style, and both read one pyproject.toml section. Built-in caching means unchanged files are skipped, so warm runs finish in milliseconds.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can Ruff fully replace Flake8, Black, and isort?
02
How do I stop Ruff suggesting syntax my runtime rejects?
03
What is the difference between ruff check and ruff format?
04
How should Ruff run in pre-commit hooks?
05
How do I roll Ruff out to a large legacy codebase?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
September 22, 2026
last updated
1,799
articles · all by Naren
🔥

That's Tooling. Mark it forged?

3 min read · try the examples if you haven't

Previous
Python uv Packaging and Workflows
1 / 1 · Tooling
Next
Pandas Row Iteration and Vectorization