Skip to content

0.16.0 removed 18 rules from the default set without recording it in BREAKING_CHANGES.md #27199

Description

@blightbow

Summary

The 0.16.0 default rule set is documented as an expansion, 413 rules up from 59. It is also a contraction: 18 rules that were enabled by default in 0.15.8 are not in the new default set. All 18 are stable, not deprecated, and not removed, so they are still perfectly good rules. They simply stopped running for anyone who was relying on the default.

I do not think the removals are wrong. They may well be deliberate and correct. My concern is only that they are not written down anywhere, so the people affected have no way to find out except by diffing the two rule sets themselves.

The 18:

E401 E402 E701 E702 E703 E711 E712 E713 E714
E721 E731 E741 E742 E743 F403 F405 F406 F722

Impact scope

Projects with no lint configuration, and projects using extend-select, since that extends the default rather than replacing it. Projects using select are unaffected, because select replaces the default outright.

That second group is the awkward one. The migration blog's only line addressing existing configuration is:

Even if you're already using select or extend-select, we hope that this will draw your attention to helpful rules that you previously hadn't discovered.

Read as guidance, that suggests there is nothing to do beyond browsing the additions. An extend-select user who acts on it silently loses 18 rules.

Reproduction

$ echo "x = 1" > probe.py
$ for v in 0.15.8 0.16.0; do
    uvx ruff@$v check --isolated --no-cache --show-settings probe.py \
      | sed -n '/^linter.rules.enabled = \[/,/^\]/p' \
      | grep -oE '\([A-Z]+[0-9]+\)' | tr -d '()' | sort -u > enabled-$v.txt
  done

$ wc -l < enabled-0.15.8.txt
59
$ wc -l < enabled-0.16.0.txt
413

$ comm -23 enabled-0.15.8.txt enabled-0.16.0.txt | tr '\n' ' '
E401 E402 E701 E702 E703 E711 E712 E713 E714 E721 E731 E741 E742 E743 F403 F405 F406 F722

I have not included a playground link because the claim is a difference between two ruff versions, and the playground runs one. Every command above uses --isolated, so no configuration of mine is involved in any of it.

Test for consolidation

I checked whether the removals were a consolidation, with newer default rules absorbing the old ones. This seems to not be the case. I used the following file as input:

x = 1
a = 1
b = 2

if x == None:
    pass
if x == True:
    pass
if not x in [1]:
    pass
if not x is None:
    pass
if type(a) == type(b):
    pass


def g() -> "int int":
    return 1

Under 0.16.0 defaults:

$ ruff check --isolated --no-cache dropped.py
All checks passed!

Now a test with six of the dropped rules selected:

$ ruff check --isolated --no-cache dropped.py --select E711,E712,E713,E714,E721,F722
dropped.py:5:9: E711 Comparison to `None` should be `cond is None`
dropped.py:7:4: E712 Avoid equality comparisons to `True`; use `x:` for truth checks
dropped.py:9:8: E713 Test for membership should be `not in`
dropped.py:11:8: E714 Test for object identity should be `is not`
dropped.py:13:4: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
dropped.py:17:12: F722 Syntax error in forward annotation: Unexpected token at the end of an expression
Found 6 errors.

I ran the same check across all 18 and the result was the same, zero reported under defaults, all 18 reported when re-selected.

F722 is the one I would flag for a second look regardless of the documentation question. A syntax error in a forward annotation is the kind of thing the release notes single out as worth catching by default, and it is now silent unless the user opts back in.

Prior art

From BREAKING_CHANGES.md for 0.1.0, "Remove formatter-conflicting rules from the default rule set" (#7900):

Previously, Ruff enabled all implemented rules in Pycodestyle (E) by default. Ruff now only includes the Pycodestyle prefixes E4, E7, and E9 to exclude rules that conflict with automatic formatters. Consequently, the stable rule set no longer includes line-too-long (E501) and mixed-spaces-and-tabs (E101). [...] This change only affects those using Ruff under its default rule set. Users that include E in their select will experience no change in behavior.

That entry names the removed rules, gives the reason, and tells readers whether they are affected. This creates an expectation that similar rule drops that aren't folded into consolidations would be documented here.

The 0.16.0 entry in the same file is titled "New default rules" and reads, in full:

Ruff now enables a much larger set of rules by default (413, up from 59). See the blog post for more details and the new Default Rules page for a full listing of the enabled rules.

The Default Rules page it points to is a list of what is enabled, which does not help a reader work out what stopped being enabled. As far as I can tell the removals are unmentioned in BREAKING_CHANGES.md, the 0.16.0 release notes, the migration blog post, and the Default Rules page.

Before filing I checked whether narrowing the default set is routine, as documenting this in BREAKING_CHANGES.md might have fallen out of practice and would have made this issue pedantic. I dumped the enabled rule set for every minor release from 0.1.0 to 0.16.0 and diffed each against its predecessor:

0.1.0 through 0.7.0    60 rules
0.8.0 through 0.15.0   59 rules
0.16.0                 413 rules

0.7.0  -> 0.8.0    1 rule left the default set
0.15.0 -> 0.16.0   18 rules left the default set

The set has been largely stable since 0.1.0. 0.8.0 is the outlier, but it is not a coverage reduction. This is where E999 syntax-error became unconditional rather than selectable, and 0.16.0 still reports syntax errors under --isolated with no configuration:

$ printf 'def broken(\n' > syn.py
$ ruff check --isolated --no-cache syn.py
Found 1 error.

Net result: 0.16.0 is the first genuine reduction of the default rule set since 0.1.0, and a deviation from the documentation practice.

Due diligence

It goes without saying that adding a default rule is a loud change. Removing one is silent and goes unnoticed.

I looked for documentation of the rule narrowing before filing this issue:

It's possible that I missed something here, but I've attempted due diligence.

Conclusion

I believe that a BREAKING_CHANGES.md entry for 0.16.0 in the shape of the 0.1.0 one is warranted: the list of removed rules, a sentence on the reasoning, and a note on who is affected, specifically calling out extend-select users. Mirroring it into the release notes and the migration blog would help, since that is where people upgrading will look first.

If any of the 18 were dropped by oversight rather than by decision (F722 seems the most likely candidate), that would be worth a separate look.

Version

ruff 0.16.0 (a2635fd8f 2026-07-23)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions