top of page

Simon Willison Hit Ruff v0.16.0 CI Failures. The Defaults Had Changed

Jul 26
12 min read

Simon Willison found several CI jobs failing after Ruff v0.16.0 expanded its default lint rules from 59 to 413. His unpinned Ruff development dependency had silently pulled the new release into existing Python projects.

Astral released the version on July 23, 2026. Two days later, Willison described how the update reached his builds without a deliberate upgrade. The failures turned a linter release into a practical warning about dependency management.

The central conflict is not stricter linting versus weaker code. It is Ruff’s improving zero-configuration safety versus the stability developers expect from unchanged repositories. That tension matters wherever CI installs the latest available developer tools on every run.

Ruff v0.16.0 Changed What “Default” Means

The most consequential Ruff v0.16.0 change is not a new command. It is a much broader definition of what unconfigured projects should consider an error.

Ruff is a Python linter and formatter written in Rust. A linter analyzes source code for mistakes, suspicious patterns, and selected style problems without executing the program.

Before this release, Ruff enabled 59 rules when a project supplied no explicit lint selection. Version 0.16.0 enables 413 rules under the same condition, according to Astral’s migration guide.

That is an increase of 354 active checks. It also means a default Ruff installation now evaluates almost seven times as many rules as before.

The surrounding catalog has grown as well. Ruff supported 708 rules when its defaults last changed in version 0.1.0. Astral says the current collection contains 968 rules.

The old defaults primarily selected portions of Pyflakes and pycodestyle. Projects could adopt Ruff without immediately making detailed choices about its many integrated rule families.

That conservative baseline helped Ruff fit into existing repositories. It also created a widening gap between what the tool knew and what it reported automatically.

Astral closed much of that gap in version 0.16.0. The new defaults draw from additional families, including flake8-bugbear, pyupgrade, and Ruff’s own RUF category.

Flake8-bugbear focuses on likely bugs and questionable design patterns. Pyupgrade identifies syntax and standard-library patterns that can be modernized for a project’s supported Python version.

The default rule list now includes checks that can expose syntax problems and immediate runtime errors. These are not merely preferences about spacing or naming.

That distinction explains why Ruff CI failures can deserve attention. Some new reports reveal defects that previously passed only because Ruff did not enable the relevant detector automatically.

Other reports will concern maintainability, modernization, or patterns a team intentionally accepts. A larger default set cannot know every repository’s compatibility requirements or design conventions.

Ruff v0.16.0 therefore changes two things at once. It increases automatic defect detection, and it moves more policy decisions into the first upgrade after July 23.

The release also makes Ruff format Python code blocks inside Markdown files by default. Supported fenced blocks include python, py, python3, py3, pyi, and pycon.

That behavior matters for repositories containing documentation, tutorials, or Quarto notebooks. A formatting check can now identify changes outside conventional .py files.

Ruff’s release notes also describe new suppression comments and richer diagnostic output. These improvements help developers handle the additional findings once they appear.

The scope change remains the main migration event. A command that behaved predictably last week can return a nonzero exit code today against identical source code.

Why Simon Willison’s CI Failures Matter

Simon Willison’s experience shows how a developer-tool update can change a repository’s effective policy without changing the repository itself.

Willison is an independent developer and writer known for projects involving Python, data tools, and generative AI. He also co-created the Django web framework earlier in his career.

On July 25, Willison wrote that his “various CI jobs” had started failing. He traced the failures to the new Ruff defaults and an unpinned "ruff" development dependency.

His Ruff account gives the release a concrete user perspective. The repository code had not necessarily regressed, but its validation environment had changed underneath it.

A CI job, or continuous integration job, runs automated checks whenever developers propose or merge changes. Teams depend on consistent results to decide whether code is safe to accept.

If a job installs ruff without a version constraint, the package resolver can select the newest available release. The next build may then enforce behavior that no maintainer explicitly reviewed.

This failure mode is easy to dismiss because Ruff is usually a development dependency. It does not normally ship inside the application that serves users.

Yet development dependencies control whether software can move through its delivery pipeline. A new linter exit code can block a pull request, halt a release, or consume hours of investigation.

The incident also exposes a misleading distinction between runtime dependencies and tooling. Runtime packages affect what deployed software does, while tools affect whether developers can deploy it at all.

Both can introduce operational change. They simply act at different points in the system.

Willison’s experience is especially useful because Ruff’s expanded rules were working as released. The failures did not require a corrupted package, compromised registry, or defective installer.

The tool installed successfully. It inspected the project correctly under its new policy. CI failed because that policy differed from the one the repository had implicitly relied upon.

That makes this a reproducibility problem. A reproducible build or check should produce equivalent results from the same source and declared inputs.

“Latest Ruff” is not a stable input. It is a moving request whose meaning depends on when a package manager resolves it.

Lockfiles and exact constraints can make that input explicit. Renovation services can then propose controlled upgrades, letting maintainers inspect new diagnostics before merging the version change.

The lesson extends beyond Ruff. Formatters, type checkers, test runners, documentation builders, and security scanners can all revise defaults between releases.

A repository with pinned application libraries but floating developer tools remains only partly reproducible. Its production behavior may stay fixed while its path to production changes.

This concern is particularly relevant to automated coding systems. Agents often run repository checks, interpret their output, and modify code until every gate passes.

If the tools behind those gates change unexpectedly, the agent faces a moving target. It may generate unnecessary edits or suppress findings without understanding why they appeared.

Teams building a searchable engineering knowledge base can preserve upgrade decisions alongside configuration and CI history. That context helps future maintainers distinguish intentional policy from accidental drift.

Simon Willison Exposed Ruff’s New Tradeoff

Ruff’s broader defaults improve first-run coverage, but they transfer migration work to projects that treated an omitted configuration as a stable contract.

Astral’s position is straightforward. Ruff accumulated hundreds of checks while its default selection remained frozen, leaving serious diagnostics inactive for unconfigured users.

The old selection dated back to Ruff v0.1.0. Since then, the rule catalog increased by 260, from 708 to 968.

Keeping only 59 checks enabled meant Ruff’s zero-configuration experience represented a shrinking portion of its capabilities. New users could assume the default was more comprehensive than it actually was.

The release addresses that mismatch. Developers can now discover syntax errors, runtime hazards, modernization opportunities, and suspicious constructs without first studying hundreds of rule codes.

That is valuable for small projects. It also benefits new repositories that want sensible coverage before maintainers develop a detailed linting policy.

The opposing expectation is equally reasonable. Defaults are often treated as product behavior, especially when documentation presents a tool as usable without configuration.

Developers who omit lint.select may believe they are choosing Ruff’s maintained baseline. Before version 0.16.0, they were also depending on that baseline remaining stable across upgrades.

Astral changed the baseline because leaving it untouched carried its own cost. Projects could pass Ruff while containing errors that the installed binary already knew how to detect.

The tradeoff is therefore not safety against convenience. It is broader automatic protection against upgrade predictability.

A narrower default reduces surprise during upgrades but hides more findings from new users. A broader default reveals more defects but can disrupt established pipelines.

Ruff v0.16.0 chooses stronger protection for the next run. Projects wanting the earlier contract must now record that preference explicitly.

Astral provides a direct compatibility configuration:

The exact table can differ when configuration lives in a standalone ruff.toml. The important decision is the explicit rule selection, not the filename.

This setting restores the earlier default families. It gives teams breathing room without forcing them to pin version 0.15 indefinitely.

However, restoring old behavior should be a migration step, not an automatic rejection of every new check. Some failures can identify bugs worth correcting immediately.

A careful upgrade begins by capturing the complete diagnostic output. Maintainers can then group findings by rule code, severity, fix safety, and compatibility impact.

Rules that expose definite syntax or runtime problems deserve priority. Mechanical modernization findings can be reviewed separately, preferably in focused commits.

Policy-oriented checks require team judgment. A pattern might be valid for generated files, framework conventions, compatibility modules, or public APIs that cannot change casually.

Ruff supports per-file ignores and targeted suppressions for those cases. Version 0.16.0 adds ruff: ignore and ruff: file-ignore comments alongside existing noqa behavior.

Targeted suppression is usually easier to audit than a broad exclusion. It records where a rule does not fit and can include a reason for future maintainers.

Still, suppressions can become clutter when hundreds of existing violations appear at once. A project-wide rule selection may be more honest until maintainers schedule a deliberate cleanup.

The correct response depends on repository maturity. A new project can accept the broader baseline immediately, while a large legacy codebase may need staged adoption.

This is why default changes carry unusual weight. They apply one product-level judgment to projects with radically different histories and constraints.

The New Defaults Are Only Part of the Migration

Teams that fix the first wave of diagnostics still need to review Markdown formatting, machine-readable output, and suppression behavior.

Ruff v0.16.0 makes Python code blocks in Markdown part of its formatter’s normal reach. That can change READMEs, documentation pages, and notebook-like publishing files.

The formatter recognizes common Python information strings attached to fenced code blocks. It handles pyi as stub code and pycon as an interactive Python session.

Quarto users can also format blocks marked with forms such as {python}. Projects using .qmd files may need an extension mapping before Ruff includes them.

This feature aligns documentation examples with the source formatter. That reduces the chance that copied examples use stale or inconsistent formatting.

It can also produce unexpected CI failures when ruff format --check previously examined only conventional source files. Documentation owners may encounter Ruff policy for the first time.

Projects can exclude Markdown with extend-exclude if needed. They can also use formatting suppression comments around selected regions.

The decision should reflect whether code samples are executable guidance or carefully arranged explanatory material. Automated formatting helps the first category more consistently than the second.

Diagnostic rendering has changed too. Ruff now shows suggested diffs within normal check and format --check output.

Previously, developers could request a diff separately. The new full output keeps diagnostics and proposed changes together, making a failed check easier to interpret.

For CI providers, format --check now supports output formats used for GitHub and GitLab annotations. A formatting problem can appear directly on the affected line in a code review.

Machine consumers need closer attention. Several fields in Ruff’s JSON output can now be null instead of containing placeholder locations.

The affected fields include filename, location, end_location, and corresponding locations inside fix edits. Consumers that assume every location is an object or string may fail.

This is a small breaking change for most users. It is more important for teams that parse Ruff output into dashboards, review bots, or custom quality systems.

A pipeline can therefore fail at three layers after the update. Ruff can find a new violation, formatting can expand to a new file type, or an output parser can reject nullable fields.

Treating every failure as “more lint rules” risks missing the actual cause. Maintainers should identify which layer changed before editing application code.

The new suppression format also deserves policy review. A line-ending ruff: ignore[F401] behaves like a targeted noqa for that diagnostic.

A preceding comment can suppress findings on the next logical line. That is useful for multi-line function headers where the reported issue does not fit neatly beside the relevant token.

File-wide suppression is available through ruff: file-ignore. It can include a reason, which gives reviewers more information than an unexplained blanket exclusion.

The new --add-ignore option can insert a suppression automatically. That convenience should not replace review of whether the underlying diagnostic represents a real defect.

Automation can quickly make CI green by adding comments. It cannot decide whether a project should carry that exception for years.

Ruff separates fixes considered safe from those requiring an unsafe-fix option. Even a safe classification should be reviewed in the context of generated code, public APIs, and unusual runtime behavior.

Python’s dynamic behavior limits what static analysis can guarantee. Ruff’s own fix guidance asks users to report cases where a safe fix damages code.

That limitation does not weaken the case for linting. It reinforces the need to distinguish detection, automated modification, and human approval.

Unpinned Developer Tools Are Now the Pressure Point

The immediate pressure falls on repositories that install Ruff dynamically while leaving rule selection implicit.

A fully pinned Ruff version with an explicit select list has two stable controls. One fixes the tool implementation, while the other fixes the project’s chosen lint policy.

A floating version with explicit rules has partial stability. New Ruff releases can still alter individual rule behavior, parsing, output, formatting, or configuration semantics.

A pinned version without explicit rules also has partial stability. CI stays consistent until maintainers update Ruff, when the default migration arrives all at once.

An unpinned version without explicit rules has neither control. That combination produced the conditions behind Simon Willison’s Ruff CI failures.

Pinning does not mean freezing the tool indefinitely. It separates discovering an upgrade from adopting it.

A dependency update pull request creates a visible review boundary. CI can show the new findings while the existing main branch remains reproducible.

Maintainers can then choose among several responses:

  • Fix clear defects identified by the new defaults.

  • Accept safe mechanical changes in isolated commits.

  • Configure intentional exceptions for repository-specific patterns.

  • Restore the earlier selection and schedule staged rule adoption.

  • Update parsers that cannot handle nullable JSON locations.

  • Exclude documentation files that must preserve manual formatting.

These actions should not be mixed blindly. A single large autofix commit can conceal behavioral changes among thousands of formatting edits.

Grouping work by rule family produces clearer review. It also makes rollback easier if one rule conflicts with the project’s supported Python versions.

Target-version configuration matters when pyupgrade rules are active. Modern syntax can be correct for one interpreter baseline and unusable for another.

Teams should verify that Ruff’s configured Python target matches deployment reality. Otherwise, modernization advice can drift ahead of production support.

Generated code needs separate treatment as well. Reformatting or linting generated files often creates changes that disappear the next time the generator runs.

Excluding generated paths can be more accurate than filling them with suppression comments. The generator’s source or template is usually the proper place to enforce quality.

Monorepositories face another complication. Different packages may support different Python versions or maintain distinct lint policies.

A root-level default can simplify operations, but it can also impose one migration schedule on unrelated components. Per-package configuration may better reflect ownership.

The main skeptical question is whether 413 rules form a broadly acceptable baseline. Astral has documented the selection, but real-world adoption will test its false-positive rate and compatibility burden.

Willison’s failures provide one early signal, not a representative survey. They show that disruption is possible, not that most Ruff users will experience it.

Projects already using an explicit select or extend-select can respond differently. Their effective rule set depends on how that configuration interacts with the new baseline.

Astral says the change can still surface helpful rules for configured users. Each team should inspect the resolved selection instead of assuming configuration makes the release irrelevant.

There is also a risk of overcorrecting. Pinning Ruff while leaving every other developer tool floating solves only one visible instance of a broader problem.

Teams should inventory formatters, type checkers, test tools, pre-commit hooks, and documentation builders. Any of them can change a clean build into a failure.

The durable policy is simple: version the environment that decides whether code may ship. That policy includes the tools developers traditionally classify as optional.

What Simon Willison and Ruff Users Should Watch Next

The next three signals will show whether Ruff’s broader baseline becomes accepted policy or a recurring source of CI friction.

The first signal is Astral’s patch-release activity during the weeks after version 0.16.0. Rapid adjustments to default rules would indicate that real repositories uncovered significant compatibility problems.

Rule-specific corrections would not invalidate the expanded baseline. They would show that a much larger selection needs tuning under production workloads.

Conversely, limited rollback activity would strengthen Astral’s case that most new diagnostics are actionable. It would also encourage more projects to accept the defaults instead of restoring the earlier set.

The second signal is configuration behavior across public Python repositories. Maintainers will reveal their judgment through commits, even without formal surveys.

A wave of explicit old-default selections would suggest that teams value migration control over immediate coverage. Widespread fixes and retained defaults would point toward successful adoption.

The most informative repositories will document their reasoning. A bare ignore list shows what changed, while a migration note explains why a team accepted or rejected each rule family.

The third signal is whether package templates and CI examples begin pinning Ruff. New-project generators often shape habits more effectively than retrospective warnings.

If templates adopt version constraints and automated update workflows, Willison’s experience will have influenced practice beyond this single release.

If examples continue installing an unconstrained ruff, future default or formatter changes can repeat the same surprise. The specific rule count will differ, but the reproducibility problem will remain.

Developers do not need to wait for those signals before acting. They can run the new version against a branch, preserve the output, and decide which findings improve their code.

A useful test command is:

Specifying the version makes the experiment repeatable. Running ruff format --check . separately helps distinguish lint failures from Markdown or source-formatting changes.

Do not begin by adding global ignores. First identify which rules found definite bugs, which suggest modernization, and which encode a debatable policy.

Then record the decision in configuration and version control. A green CI badge is less valuable when nobody knows which quality contract produced it.

Ruff v0.16.0 demonstrates the advantage and cost of active defaults. The tool catches more without configuration, but omitted configuration no longer means unchanged behavior.

Simon Willison’s experience turns that abstract tradeoff into an immediate engineering question: does your repository declare the tools and policies that control its releases?

Run the pinned version on a branch, inspect every new rule family, and make the baseline explicit. The next clean build should reflect a reviewed decision, not the date CI happened to install Ruff.

Give every agent the context to do better work

Connect your agents to the knowledge, decisions, and history already organized in remio.

remio currently supports Windows 10+ (x64) and Macs with Apple silicon.

Your AI Partner at Work
Get more done with remio

Plan. Create. Deliver.
All in one place.

bottom of page