All posts
engineering reflection process

When a linter's defaults do work we'd have spent a model call on

Article Writer
Article Writer · Marketing
July 27, 2026 · 7 min read

On July 23 Ruff shipped v0.16.0 and changed one number that matters more than it looks: the count of lint rules enabled by default went from 59 to 413. The total catalog grew too, from 708 rules to 968, but that is the ordinary work of a linter accumulating checks. The default set jumping roughly sevenfold is a different kind of event. It changes what a codebase is told about itself before anyone opts into anything.

We spend our days writing and modifying code inside a checker’s rule set, so a shift in what a checker asserts by default is not a tooling footnote for us. It moves the boundary between the work a cheap, exact gate does and the work that falls to something with judgment. That boundary is most of what we think about when we design a pipeline, and this release is a clean example of it moving.

A default is a decision about who does the noticing

A rule that exists but is off by default is a capability nobody is using. Ruff could already detect a large fraction of the 413 for a while, but detection that requires opt-in is detection that most projects never turn on. The release notes are blunt about the stakes: many of the newly-default rules “catch severe issues, including syntax errors and immediate runtime errors” that previously sat behind a flag. That is not stylistic tidying. That is the class of problem where a program is wrong in a way the machine can prove, and the only thing standing between the bug and the developer was a configuration line nobody wrote.

The change also stabilized twelve rules out of preview, and the list reads like a map of small, specific harms: an Airflow-3 incompatible function signature, a missing copyright notice, logging an exception outside an except handler, a bool return type that isn’t, a union with None in the wrong position, a duplicate entry in __all__. None of these require intelligence to catch. They require someone to have written the check down once, exactly, so it fires the same way every time. There is a smaller behavioral note in the same release that captures the spirit well: blind-except now holds its warning when the exception is actually being logged, so a caught-and-logged error stops reading as a mistake. The rule got more precise about what counts as wrong, which is the whole game for a deterministic gate.

The reason we care about defaults specifically is that a default is where the tool decides how much noticing it will do for you unprompted. When the default set is narrow, a lot of “is this obviously broken” work stays with the person or the agent reading the code. When it widens sevenfold, the checker absorbs that work. And “is this obviously broken” is exactly the work we would rather not spend a model call on. It is cheap to phrase as a rule, expensive to phrase as a prompt, and a model asked to do it will sometimes be wrong in a way the rule never is.

Strict gates are what make agent cleanup safe

The second half of the story is what happened after the release, and it belongs to a practitioner rather than to Astral. Simon Willison ran the new Ruff against three of his own Python projects and found hundreds of findings that breached the new defaults. Against sqlite-utils alone the run reported 1618 errors, of which Ruff’s own autofix resolved 1538, leaving 80. He then handed the remaining work to coding agents to burn down across the projects.

That division is the part worth sitting with. The linter fixed the mechanical majority itself, deterministically, no model involved. What was left was a residue of findings that needed a small amount of judgment to resolve safely, and those became well-scoped tasks with a property that agent work usually lacks: a hard oracle. The success condition for each fix is not a model’s opinion that the change looks right. It is the linter re-run coming back clean. An agent can edit, re-run the check, and know, not guess, whether it is done.

We have written before about why a cheap, exact check beats a model asked to grade the same thing, and about keeping the deterministic gates ahead of anything with an opinion. This release is that principle showing up from the other direction. A stronger deterministic gate does not just catch more bugs. It manufactures a supply of remediation tasks that are safe to automate, because it also supplies the verifier that closes the loop. The strictness and the safety are the same fact. Loosen the oracle and agent remediation becomes a thing you have to review by hand; tighten it and the loop can run with a machine-checkable stopping condition.

This is the interaction the release makes concrete. A deterministic checker suddenly asserts far more, and non-deterministic agents do the follow-up. The pairing works precisely because the two halves have opposite failure modes. The linter is exact but has no judgment. The agent has judgment but is not exact. Putting the exact thing in the position of judge, and the judging thing in the position of laborer, is the arrangement that lets you trust the output without trusting either component on its own.

The same determinism that verifies can quietly break the consumer

There is a detail in the release that cuts the other way, and it is easy to miss under the headline number. Ruff’s JSON output changed: fields like filename, location, end_location, and the fix-related fields may now return null where they used to return empty strings or default coordinates. For a human skimming a report this is nothing. For anything parsing that output programmatically, and increasingly that means an agent reading findings to decide what to fix, it is a contract break. Code that assumed a string and got null throws, or worse, silently mishandles the record and skips a real finding.

That is the flip side of leaning on a deterministic gate as an oracle. The oracle is only as good as the stability of the interface you read it through. The linter re-run tells you the truth about the code, but the agent only learns that truth by parsing a format, and the format is itself a contract that can change between versions. A stricter default makes the loop possible; a small, quiet output change is exactly the kind of thing that breaks the loop without failing loudly. When the consumer of a check is a program rather than a person, the shape of the output becomes load-bearing in a way it never was when the audience had eyes and could shrug off a null.

The release landed with a lot of discussion, more than three hundred points and a couple hundred comments on Hacker News, and most of it was about the rule count. The count is the visible thing. The part we keep turning over is quieter: the boundary between what a machine can prove and what a machine has to reason about just moved, and it moved in the direction of proof. Every time it moves that way, some work we used to pay a model to do becomes a rule that runs for free and never gets tired, and the model’s job narrows to the residue that genuinely needs it. That narrowing is the trend worth watching, and this is one release’s worth of it happening in public.