Ruff 0.16 is not a drop-in upgrade if your project relies on Ruff's implicit lint defaults. The default rule set grew from 59 to 413 rules, while 18 opinionated E and F rules left that default set. The safe approach is to pin the new Ruff version, compare diagnostics before changing code, and make your intended rule selection explicit.
This guide demonstrates that migration with Ruff 0.16.3. It also tests the new Markdown formatter, which can format Python fenced code blocks while leaving non-Python fences alone.
What Changed in Ruff 0.16
Ruff 0.16.0 was released on July 23, 2026. Its most consequential migration change is a redesigned default rule set. Ruff's official release post says that the default now enables 413 rules instead of 59.
The new defaults include useful checks from categories such as B, UP, and RUF. However, this is not merely a superset of the old defaults. Ruff also removed 18 opinionated E and F rules from the default selection, including E712, which warns about comparisons such as flag == True.
That distinction matters in both directions:
- A previously clean project may gain new diagnostics.
- A diagnostic that used to appear implicitly may disappear.
- A project with an explicit
lint.selectis insulated from most default-set changes, because its configured selection remains the source of truth.
Ruff 0.16 also stabilized formatting for Python code inside Markdown. Fences labelled python, py, python3, py3, pyi, or pycon are recognized. A plain text fence is not treated as Python.
Reproduce the Default-Rule Difference
The following file intentionally contains one modern-typing issue and one old-default style issue:
from typing import List
def mode(flag):
if flag == True:
return "enabled"
else:
return "disabled"
def labels() -> List[str]:
return ["stable"]
Run Ruff without project configuration:
ruff check --isolated legacy.py
With Ruff 0.16.3, the tested command exited with code 1 and reported two diagnostics:
UP035for importing deprecatedtyping.List.UP006for usingList[str]instead oflist[str].
It did not report E712, because that rule is no longer part of the implicit default set.
Now reproduce the previous default selection published in Ruff's migration guidance:
ruff check --isolated --select E4,E7,E9,F legacy.py
That command also exited with code 1, but it reported only E712. It did not enable the two UP diagnostics.
Why This Is a Better Migration Test
A single before-and-after count can be misleading. A project might still report one error even though the identity and purpose of that error changed. Compare rule codes and affected files, not just the total number of diagnostics.
The --isolated flag is useful for a controlled experiment because it ignores discovered configuration files. Do not add it blindly to normal CI: production linting should normally use the repository's reviewed configuration.
Choose Your Migration Strategy
There are two sensible paths. Pick one deliberately instead of allowing an unreviewed tool update to choose for you.
Preserve the Previous Defaults First
For a low-risk upgrade, pin Ruff 0.16 and temporarily make the old selection explicit:
[tool.ruff]
target-version = "py312"
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F"]
This preserves the former default categories while you evaluate the new rules separately. It is a migration bridge, not necessarily the best permanent configuration.
Run the checks your repository already trusts:
ruff check .
ruff format --check .
After that baseline is stable, inspect Ruff's current Default Rules page and add categories or individual codes that match your project. Review fixes in small commits. Avoid enabling ALL merely to chase a larger rule count; rule selection should reflect useful, maintainable checks.
Adopt the New Defaults Deliberately
If you want Ruff's expanded defaults, remove any temporary legacy selection and run the new version across the repository. Capture the resulting diagnostics before applying fixes:
ruff check .
ruff check --fix --diff .
The second command previews safe fixes as a diff rather than rewriting files. Review each category, especially in generated code, compatibility layers, migrations, and vendored sources. Exclude generated or third-party paths instead of scattering suppressions through files you do not maintain.
Some fixes are classified as unsafe because they can change behavior. Do not add --unsafe-fixes to CI without reviewing why each selected rule is safe for the codebase.
Test Markdown Code Formatting
Ruff 0.16 can format Python examples in documentation. Consider a README.md whose python fence contains:
def greeting( name:str ):
return f"Hello, {name}!"
The same document has a text fence containing:
def untouched( name:str ):
return name
Preview the change without writing it:
ruff format --isolated --check --diff README.md
In the Ruff 0.16.3 test, the command exited with code 1 and showed this Python-fence change:
-def greeting( name:str ):
+def greeting(name: str):
Applying the formatter succeeded:
ruff format --isolated README.md
The python fence was reformatted, while the intentionally untidy text fence remained unchanged. This is useful for keeping executable examples consistent with application code, but it can create a large documentation diff on the first run.
Control the Documentation Rollout
Start with ruff format --check --diff README.md or a small documentation directory. If some examples intentionally demonstrate invalid or unusually formatted code, surround the relevant region with the documented HTML suppression comments:
<!-- fmt: off -->
<!-- intentionally preserved example -->
<!-- fmt: on -->
To disable Markdown formatting repository-wide, add a Markdown glob to extend-exclude. For custom documentation extensions such as .mdx or .qmd, use Ruff's extension mapping rather than renaming files.
If Ruff runs through ruff-pre-commit, Markdown files must also be included in that hook's types_or list. A direct ruff format . invocation and a pre-commit hook can otherwise cover different file sets.
Build a Reviewable CI Upgrade
Keep the tool upgrade, configuration decision, and mechanical fixes easy to audit:
- Pin a reviewed Ruff 0.16 patch release in the package, tool, or pre-commit configuration used by CI.
- Record the previous
ruff check .andruff format --check .results. - Upgrade the pin without applying fixes and compare rule codes and files.
- Decide whether to preserve the old set or adopt selected new defaults.
- Preview fixes with
--diff, then commit reviewed mechanical changes separately. - Roll out Markdown formatting on a bounded path before including every document.
A minimal CI gate remains straightforward:
ruff check .
ruff format --check .
The important reproducibility control is the version pin. Using ruff@latest in a verification job allows a later default or formatter change to alter CI without a repository change. Dependency-update tooling can still propose upgrades, but the pull request should contain the version transition being reviewed.
Do Not Hide the Migration with Blanket Ignores
An ignore can be appropriate when a rule conflicts with a documented project constraint. Prefer a narrow, explained configuration ignore or Ruff's supported suppression comments. A repository-wide ignore added only to make the upgrade green discards the value of the new diagnostic and leaves future maintainers without context.
Ruff 0.16 introduced ruff: ignore and ruff: file-ignore forms in addition to existing noqa-style controls. Use the narrowest scope and include a reason when the exception is not obvious.
Tested Results and Limits
The examples were executed on September 6, 2026 with Ruff 0.16.3 on Windows x64:
- New implicit defaults: exit
1, withUP035andUP006. - Explicit former defaults: exit
1, withE712only. - Markdown
--check --diff: exit1, showing the expected Python-fence formatting change. - Markdown formatting: exit
0; the Python fence changed and thetextfence did not.
This validation used isolated sample files, not a representative large repository. No performance benchmark is claimed. The article's counts and migration behavior come from Ruff's official 0.16 release guidance; the concrete diagnostics and formatting output were reproduced locally.
Ruff 0.16 is a valuable upgrade, but treating its defaults as an implicit contract makes future transitions harder. Pin the tool, make rule intent visible, inspect the diagnostic identities, and introduce documentation formatting with a reviewable diff.


Comment