Ruff Flips On 413 Default Rules and Breaks Your CI
Astral's first defaults change since 2023 ends the flake8-clone era, and it's the right call.
If your Python CI went red this week, you already know: Ruff v0.16.0, released July 23rd, enables 413 lint rules by default, up from 59. That's not a tweak. It's the largest behavioral change Astral has ever shipped to the tool, and it lands automatically on every project with an unpinned ruff dependency — which, judging by the volume of broken builds being reported, is a lot of projects.
The knee-jerk take is "breaking change in a minor version, bad Astral." The more interesting take is that this release quietly ends Ruff's first identity and starts its second.
Defaults are the product
Ruff's default rule set had been frozen since v0.1.0 in late 2023. Back then the defaults — E4, E7, E9, and F — were a deliberate mimicry of flake8's out-of-the-box behavior. That was the pitch: a drop-in replacement for flake8 plus dozens of its plugins, 100x faster, zero migration cost. Conservative defaults were the point.
But while the defaults sat still, the tool didn't. Ruff's total rule count grew from 708 to 968, and the gap between "what Ruff can catch" and "what Ruff catches unless you configure it" became absurd. Rules that flag genuine bugs — mutable default arguments, naive datetimes, broken async patterns, code that raises at runtime — were sitting behind an opt-in flag that most teams never touched. And most teams never touch it. The dirty secret of linter configuration is that the majority of projects run whatever the tool does by default, forever. A rule that isn't on by default may as well not exist for 80% of the ecosystem.
Astral clearly ran that math. The new default set spans 34 rule categories: flake8-bugbear's 29 rules against footgun patterns, all of pyupgrade's syntax modernization (42 rules — say goodbye to Optional[X]), flake8-simplify, a large slice of Pylint, comprehension optimizations, timezone-aware datetime checks, import sorting via the isort rules, async correctness checks, and Ruff's own RUF category. This is no longer flake8's worldview. It's Astral's.
That's the identity shift. Ruff v0.16 stops being a faster flake8 and starts being Python's answer to Rust's Clippy: an opinionated, batteries-included linter whose defaults are the style guide. Given that Astral also ships uv and is building the ty type checker, the trajectory is obvious — a coherent, opinionated toolchain where the defaults do the standardizing, the way gofmt did for Go. The Hacker News thread's loudest complaint ("why is a tool this dominant still 0.x, and why do minor versions break me?") is fair, but it misreads the strategy. Astral is using the 0.x runway to make exactly these kinds of moves before a 1.0 locks them in. This was probably the last window in which a defaults change this large was even possible.
What to actually do about it
First, the immediate fix if you're broken right now. Pin your linter — a linter is a dependency like any other, and an unpinned linter in CI is a time bomb you chose to install:
[dependency-groups]
dev = ["ruff==0.16.*"]
If you want the old behavior back verbatim, the escape hatch is one line:
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F"]
But before you reach for that, check what the new defaults actually find. Simon Willison ran the new version against sqlite-utils and got 1,618 violations — of which ruff check --fix --unsafe-fixes cleared 1,538 automatically. That ratio matches the composition of the new set: pyupgrade and comprehension rules are almost entirely auto-fixable, and bugbear fixes are mostly mechanical. The stuff requiring human judgment — Pylint complexity rules, exception-handling patterns — is a thin tail.
For that tail, v0.16 ships better tooling than the old grind of scattering # noqa by hand. There's a new suppression syntax (ruff: ignore, plus ruff: disable/enable ranges and ruff: file-ignore), and an --add-ignore flag that writes the suppressions for you. The pragmatic migration for a large legacy codebase: upgrade, auto-fix everything fixable, --add-ignore the remainder, then burn down the suppressions at leisure. That's an afternoon, not a sprint.
One audit worth doing: if your config uses extend-select to add categories like B or UP, those lines are now partially redundant — but not entirely, since the defaults enable curated subsets of each category, not the whole thing. Don't delete config blindly.
The AI wrinkle
There's a reason this change lands easier in 2026 than it would have in 2023, and Willison's writeup demonstrates it without quite saying it: he pointed coding agents at the residual violations and let them churn through Ruff's (genuinely excellent) diagnostic messages. Fixing 1,600 lint findings used to be the kind of toil that made teams veto stricter rules. That veto is losing its force when an agent clears the backlog overnight.
The arrow points the other way too. Teams are merging more machine-generated Python than ever, and a strict, fast, deterministic linter is exactly the guardrail that setup needs — it catches the plausible-looking-but-wrong patterns LLMs love, and it does so in milliseconds rather than in code review. Aggressive defaults make Ruff a better backstop for AI-assisted development precisely when the ecosystem needs one. I doubt that timing is accidental.
Verdict
This is the right change, shipped about as well as a breaking defaults change can be: a one-line revert, strong auto-fix coverage, new suppression tooling, and a real migration doc. The breakage stories are real, but nearly all of them reduce to "my linter wasn't pinned," which is a lesson worth the outage. New projects get a genuinely good rule set for free; existing projects get a forcing function most of them needed anyway.
The people with a legitimate grievance are teams running Ruff in CI at 0.x with floating versions — and the fix for them is in this paragraph. Everyone else just got a 7x better linter without editing a config file. That's what good defaults look like, even when they arrive loudly.
Sources & further reading
- Ruff v0.16.0 — astral.sh
- Ruff v0.16.0 — simonwillison.net
- Release 0.16.0 — github.com
- Ruff 0.16.0 Enables 7x More Rules by Default — pydevtools.com
- Ruff v0.16.0 - Significant new updates - 413 default rules up from 59 — news.ycombinator.com
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 2
ah so that's why our CI exploded. annoyed but...yeah, more rules by default makes sense. going to have to revisit what we actually want enabled though
yeah the annoying part is having to actually *think* about your lint config instead of just inheriting defaults. but also... fair.