2026-07-26 Ruff 0.16 Defaults and the select vs extend-select Trap
Context: python-project-template (github.com/achhina/python-project-template). Ruff 0.16.0 landed 2026-07-23 with a huge default-rule expansion; wanted the template on it. Shipped as PR #5 / v1.5.0.
What I set out to do
Bump the template’s ruff floor to >=0.16.0 and compare what 0.16 enables by default against the curated select list already in template/pyproject.toml.jinja.
What I actually did
- Measured instead of guessed. Rather than reading release notes and inferring, diffed the two rule sets empirically: rendered a config with only
[tool.ruff]and one with the template’sselect, ranruff check --show-settingson each, and diffed thelinter.rules.enabledlists. Ruff 0.16 enables 413 rules by default, up from 59. - Found the actual bug.
selectreplaces the default set rather than adding to it. So on 0.16 the curated list was silently discarding 135 default rules — the version bump alone would have changed nothing. Switched toextend-select, which layers. Generated projects went 506 → 643 rules. - Checked whether the list could shrink. The obvious follow-up was “drop the prefixes the defaults now cover.” Computed marginal contribution per prefix: none of the 19 is fully subsumed.
Sstill adds 55/58,PTH33/35,PT22/28; even the thin ones contribute (Iadds onlyI002,C4onlyC416/C420,ERAandTIDandARGadd 100% of theirs). List stayed unchanged. - The new defaults surfaced a real template bug. A default generation failed
ruff checkwithD419(empty docstring), becauseproject_descriptiondefaults to""and__init__.py.jinjawas"""{{ project_description }}""". Chasing that revealed the same root cause brokemdformattoo: the empty value left stray blank lines inREADME.md,AGENTS.md, anddocs/index.md. A default generation shipped a project that failed its own pre-commit run. - Fixed both:
or project_namefallback in the docstring, whitespace-controlled{%- if project_description %}blocks in the three markdown templates. Verified against both empty and non-empty descriptions. - Opened PR #5 bumping
template_versionto 1.5.0 with a matching CHANGELOG section, per the automated release flow.
What was striking
selectvsextend-selectis a silent-downgrade trap, not a style choice. Anyone with a curatedselectwho upgrades to 0.16 gets fewer rules than a config with no lint section at all, and nothing warns them. The curated list looks like the strict option and is now the permissive one.--show-settingsis the right tool for this class of question. It prints the fully-resolvedlinter.rules.enabledlist, so “what does this config actually turn on” becomes a diff instead of an argument. Worth remembering: it prints bothrules.enabledandrules.should_fix, so naive grepping double-counts (I did, briefly, and got 826 instead of 413).- The lint upgrade found a generation bug that lint upgrades aren’t supposed to find.
D419was the thread; the mdformat failure was the same empty-string hole one layer over. The emptyproject_descriptiondefault had been broken since forever and nothing caught it because nobody ran the generated project’s own hooks on a default generation. - Closed out a deferred item from 2026-06-29 Python Project Template Modernization and Release Automation. That entry’s “Top 3 next” #2 listed “ruff rule additions (DTZ/LOG/TC/…)” as deferred polish. Adopting the 0.16 defaults delivered exactly those —
DTZ,LOG,TC, plusASYNC,TRY,G,FURB,BLE001,T100— without hand-curating a single one. Sometimes the right move on a deferred config chore is to wait for upstream to make it the default. - That same entry’s #3 was “watch the next real version-bump PR merge auto-release end-to-end.” PR #5 is that PR — first real exercise of the release pipeline since the git-identity fix.
Top 3 next
- Merge PR #5 and confirm the release workflow cuts
v1.5.0end-to-end with no manualworkflow_dispatch(the deferred verification from June). - Consider whether existing downstream projects need a heads-up beyond the CHANGELOG “Upgrading” note —
copier updatewill surface ~137 new rules and can fail lint on previously-passing code. - Remaining June polish bucket, still open:
exclude_alsocoverage config,GPL-3.0→GPL-3.0-or-laterSPDX, dropping deadcheck-dependabot/check-readthedocshooks.
Related
2026-06-29 Python Project Template Modernization and Release Automation