Skip to content

[ruff] Add fix for none-not-at-end-of-union (RUF036) #22829

Merged
amyreese merged 12 commits intoastral-sh:mainfrom
anishgirianish:feat/ruf036-autofix
Feb 27, 2026
Merged

[ruff] Add fix for none-not-at-end-of-union (RUF036) #22829
amyreese merged 12 commits intoastral-sh:mainfrom
anishgirianish:feat/ruf036-autofix

Conversation

@anishgirianish
Copy link
Contributor

Summary

Adds an autofix for RUF036 that moves None to the end of union type annotations.

Closes #15136

The fix:

  • Preserves the union style (PEP 604 | vs typing.Union)
  • Preserves duplicate None values instead of deduplicating
  • Skips nested unions to avoid flattening (e.g., None | Union[int, str])
  • Marked unsafe when comments are present in the annotation

Test Plan

cargo nextest run -p ruff_linter - added test cases for nested unions, comments,
default arguments, and mixed styles.

@anishgirianish anishgirianish changed the title [ruff] Add fix for none-not-at-end-of-union (RUF036) [ruff] Add fix for none-not-at-end-of-union (RUF036) Jan 24, 2026
@amyreese amyreese requested a review from ntBre February 13, 2026 23:07
@anishgirianish
Copy link
Contributor Author

@amyreese Thanks for the review! I've addressed all the feedback:

  • Changed "type annotation" to "type union" in the message and fix title
  • Refactored generate_pep604_fix and generate_typing_union_fix to return Edit, building the Fix in generate_fix
  • Removed .copied() by simplifying to [other_exprs, none_exprs].concat()

Ready for another look whenever you have a chance. Thank you!

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 14, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+395 -395 violations, +0 -0 fixes in 18 projects; 38 projects unchanged)

DisnakeDev/disnake (+10 -10 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ disnake/gateway.py:287:37: RUF036 [*] `None` not at the end of the type union.
- disnake/gateway.py:287:43: RUF036 `None` not at the end of the type annotation.
+ disnake/i18n.py:103:15: RUF036 [*] `None` not at the end of the type union.
- disnake/i18n.py:103:35: RUF036 `None` not at the end of the type annotation.
+ disnake/i18n.py:113:15: RUF036 [*] `None` not at the end of the type union.
- disnake/i18n.py:113:35: RUF036 `None` not at the end of the type annotation.
+ disnake/i18n.py:137:44: RUF036 [*] `None` not at the end of the type union.
- disnake/i18n.py:137:50: RUF036 `None` not at the end of the type annotation.
+ disnake/i18n.py:30:37: RUF036 [*] `None` not at the end of the type union.
- disnake/i18n.py:30:43: RUF036 `None` not at the end of the type annotation.
... 10 additional changes omitted for project

RasaHQ/rasa (+3 -3 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ rasa/shared/core/events.py:597:42: RUF036 [*] `None` not at the end of the type union.
- rasa/shared/core/events.py:597:48: RUF036 `None` not at the end of the type annotation.
+ rasa/shared/core/events.py:624:25: RUF036 [*] `None` not at the end of the type union.
- rasa/shared/core/events.py:624:31: RUF036 `None` not at the end of the type annotation.
+ rasa/utils/tensorflow/models.py:247:29: RUF036 [*] `None` not at the end of the type union.
- rasa/utils/tensorflow/models.py:247:35: RUF036 `None` not at the end of the type annotation.

apache/airflow (+108 -108 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ airflow-core/src/airflow/api_fastapi/common/parameters.py:1086:22: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/common/parameters.py:1086:22: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/api_fastapi/common/parameters.py:203:20: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/common/parameters.py:203:20: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/api_fastapi/common/parameters.py:206:22: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/common/parameters.py:206:22: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/api_fastapi/common/parameters.py:210:26: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/common/parameters.py:210:26: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py:38:65: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py:38:65: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/example_dags/plugins/event_listener.py:102:12: RUF036 [*] `None` not at the end of the type union.
- airflow-core/src/airflow/example_dags/plugins/event_listener.py:102:12: RUF036 `None` not at the end of the type annotation.
+ airflow-core/src/airflow/executors/base_executor.py:155:13: RUF036 [*] `None` not at the end of the type union.
... 203 additional changes omitted for project

apache/superset (+4 -4 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ superset/config.py:1511:5: RUF036 [*] `None` not at the end of the type union.
- superset/config.py:1511:5: RUF036 `None` not at the end of the type annotation.
+ superset/config.py:2271:43: RUF036 [*] `None` not at the end of the type union.
- superset/config.py:2271:43: RUF036 `None` not at the end of the type annotation.
+ superset/db_engine_specs/gsheets.py:310:26: RUF036 [*] `None` not at the end of the type union.
- superset/db_engine_specs/gsheets.py:310:26: RUF036 `None` not at the end of the type annotation.
+ superset/jinja_context.py:101:10: RUF036 [*] `None` not at the end of the type union.
- superset/jinja_context.py:101:16: RUF036 `None` not at the end of the type annotation.

bokeh/bokeh (+3 -3 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select ALL

+ src/bokeh/client/websocket.py:73:85: RUF036 [*] `None` not at the end of the type union.
- src/bokeh/client/websocket.py:73:85: RUF036 `None` not at the end of the type annotation.
+ src/bokeh/embed/standalone.py:84:24: RUF036 [*] `None` not at the end of the type union.
- src/bokeh/embed/standalone.py:84:30: RUF036 `None` not at the end of the type annotation.
+ src/bokeh/util/tornado.py:231:26: RUF036 [*] `None` not at the end of the type union.
- src/bokeh/util/tornado.py:231:26: RUF036 `None` not at the end of the type annotation.

ibis-project/ibis (+2 -2 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ ibis/backends/__init__.py:1294:54: RUF036 [*] `None` not at the end of the type union.
- ibis/backends/__init__.py:1294:54: RUF036 `None` not at the end of the type annotation.
+ ibis/backends/sql/__init__.py:726:43: RUF036 [*] `None` not at the end of the type union.
- ibis/backends/sql/__init__.py:726:43: RUF036 `None` not at the end of the type annotation.

langchain-ai/langchain (+10 -10 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ libs/langchain/langchain_classic/evaluation/parsing/base.py:152:10: RUF036 [*] `None` not at the end of the type union.
- libs/langchain/langchain_classic/evaluation/parsing/base.py:152:24: RUF036 `None` not at the end of the type annotation.
+ libs/langchain/langchain_classic/evaluation/parsing/json_distance.py:94:41: RUF036 [*] `None` not at the end of the type union.
- libs/langchain/langchain_classic/evaluation/parsing/json_distance.py:94:55: RUF036 `None` not at the end of the type annotation.
+ libs/langchain/langchain_classic/evaluation/parsing/json_schema.py:67:41: RUF036 [*] `None` not at the end of the type union.
- libs/langchain/langchain_classic/evaluation/parsing/json_schema.py:67:55: RUF036 `None` not at the end of the type annotation.
+ libs/langchain/langchain_classic/indexes/_sql_record_manager.py:93:17: RUF036 [*] `None` not at the end of the type union.
- libs/langchain/langchain_classic/indexes/_sql_record_manager.py:93:17: RUF036 `None` not at the end of the type annotation.
+ libs/langchain_v1/langchain/agents/middleware/types.py:806:10: RUF036 [*] `None` not at the end of the type union.
- libs/langchain_v1/langchain/agents/middleware/types.py:806:42: RUF036 `None` not at the end of the type annotation.
... 10 additional changes omitted for project

latchbio/latch (+6 -6 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ src/latch/registry/types.py:38:43: RUF036 [*] `None` not at the end of the type union.
- src/latch/registry/types.py:47:5: RUF036 `None` not at the end of the type annotation.
+ src/latch/types/metadata.py:419:28: RUF036 [*] `None` not at the end of the type union.
- src/latch/types/metadata.py:420:5: RUF036 `None` not at the end of the type annotation.
+ src/latch_cli/services/get.py:38:26: RUF036 [*] `None` not at the end of the type union.
- src/latch_cli/services/get.py:38:32: RUF036 `None` not at the end of the type annotation.
+ src/latch_cli/snakemake/config/utils.py:200:50: RUF036 [*] `None` not at the end of the type union.
- src/latch_cli/snakemake/config/utils.py:200:56: RUF036 `None` not at the end of the type annotation.
+ src/latch_cli/snakemake/config/utils.py:25:24: RUF036 [*] `None` not at the end of the type union.
- src/latch_cli/snakemake/config/utils.py:25:53: RUF036 `None` not at the end of the type annotation.
... 2 additional changes omitted for project

milvus-io/pymilvus (+1 -1 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ pymilvus/client/search_result.py:888:51: RUF036 [*] `None` not at the end of the type union.
- pymilvus/client/search_result.py:888:57: RUF036 `None` not at the end of the type annotation.

pandas-dev/pandas (+45 -45 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

+ pandas/_libs/json.pyi:14:22: RUF036 [*] `None` not at the end of the type union.
- pandas/_libs/json.pyi:14:22: RUF036 `None` not at the end of the type annotation.
+ pandas/_libs/tslibs/timedeltas.pyi:147:36: RUF036 [*] `None` not at the end of the type union.
- pandas/_libs/tslibs/timedeltas.pyi:147:36: RUF036 `None` not at the end of the type annotation.
+ pandas/_libs/tslibs/timestamps.pyi:31:25: RUF036 [*] `None` not at the end of the type union.
- pandas/_libs/tslibs/timestamps.pyi:31:41: RUF036 `None` not at the end of the type annotation.
+ pandas/core/arrays/datetimelike.py:1075:45: RUF036 [*] `None` not at the end of the type union.
- pandas/core/arrays/datetimelike.py:1075:45: RUF036 `None` not at the end of the type annotation.
+ pandas/core/dtypes/cast.py:186:29: RUF036 [*] `None` not at the end of the type union.
- pandas/core/dtypes/cast.py:186:38: RUF036 `None` not at the end of the type annotation.
... 80 additional changes omitted for project

python/typeshed (+143 -143 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview --select E,F,FA,I,PYI,RUF,UP,W

+ stdlib/_interpreters.pyi:30:6: RUF036 [*] `None` not at the end of the type union.
- stdlib/_interpreters.pyi:30:6: RUF036 `None` not at the end of the type annotation.
+ stdlib/_ssl.pyi:82:58: RUF036 [*] `None` not at the end of the type union.
- stdlib/_ssl.pyi:82:58: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1096:51: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1096:96: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1099:51: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1099:96: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1754:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1754:26: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1764:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1764:26: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1774:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1774:26: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1784:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1784:26: RUF036 `None` not at the end of the type annotation.
+ stdlib/ast.pyi:1794:26: RUF036 [*] `None` not at the end of the type union.
- stdlib/ast.pyi:1794:26: RUF036 `None` not at the end of the type annotation.
... 268 additional changes omitted for project

... Truncated remaining completed project reports due to GitHub comment length restrictions

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF036 790 395 395 0 0

@ntBre
Copy link
Contributor

ntBre commented Feb 23, 2026

Thanks @anishgirianish for working on this! I haven't looked at the diff yet, so apologies if you've addressed all of these, but I was hoping you could check whether or not you had resolved my comment on a previous version of this PR? See #18964 (review) and the comments it links to on an even earlier version. I think that's a good summary of the missing pieces from the earlier iterations and would help me get started reviewing this PR.

@anishgirianish
Copy link
Contributor Author

Thanks @anishgirianish for working on this! I haven't looked at the diff yet, so apologies if you've addressed all of these, but I was hoping you could check whether or not you had resolved my comment on a previous version of this PR? See #18964 (review) and the comments it links to on an even earlier version. I think that's a good summary of the missing pieces from the earlier iterations and would help me get started reviewing this PR.

Sure will do thanks

@ntBre ntBre added fixes Related to suggested fixes for violations preview Related to preview mode features labels Feb 26, 2026
@amyreese amyreese self-assigned this Feb 27, 2026
@amyreese
Copy link
Member

LGTM, unless @ntBre knows of any relevant helpers that could be reused.

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This looks great to me overall, I just had one concern about fixes that generate None | None. I'd be okay just skipping fixes in cases with multiple Nones, but I'm curious what you and Amy think.

@anishgirianish
Copy link
Contributor Author

Thank you! This looks great to me overall, I just had one concern about fixes that generate None | None. I'd be okay just skipping fixes in cases with multiple Nones, but I'm curious what you and Amy think.

Thank you for the review! I've skipped the fix for PEP 604 unions with multiple Nones to avoid generating None | None. Ready for another look whenever you get a chance!

@anishgirianish anishgirianish requested a review from ntBre February 27, 2026 16:20
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! I'll leave it to Amy to merge.

@ntBre ntBre changed the title [ruff] Add fix for none-not-at-end-of-union (RUF036) [ruff] Add fix for none-not-at-end-of-union (RUF036) Feb 27, 2026
@amyreese amyreese enabled auto-merge (squash) February 27, 2026 18:56
@amyreese amyreese merged commit b02cdac into astral-sh:main Feb 27, 2026
41 checks passed
@anishgirianish
Copy link
Contributor Author

@amyreese thank you for the latest commit. I really appreciate it

carljm added a commit that referenced this pull request Feb 27, 2026
* main:
  [ty] Take myself out of the reviewer pool for the next few days (#23618)
  [ty] Fix bug where ty would think that a `Callable` with a variadic positional parameter could be a subtype of a `Callable` with a positional-or-keyword parameter (#23610)
  [`ruff`] Add fix for `none-not-at-end-of-union` (`RUF036`)  (#22829)
  Bump cargo dist to 0.31 (#23614)
  [`pyflakes`] Fix false positive for names shadowing re-exports (`F811`) (#23356)
  [`fastapi`] Handle callable class dependencies with `__call__` method (`FAST003`) (#23553)
  [ty] Recurse into tuples and nested tuples when applying special-cased validation of `isinstance()` and `issubclass()` (#23607)
  Update typing conformance suite commit (#23606)
  [ty] Detect invalid uses of `@final` on non-methods (#23604)
  [ty] Move the type hierarchy request handlers to individual modules
  [ty] Wire up the type hierarchy implementation with the LSP
  [ty] Add routine for mapping from system path to vendored path
  [ty] Implement internal routines for providing the LSP "type hierarchy" feature
  [ty] Add some helper methods on `ClassLiteral`
  [ty] Move some module name helper routines to methods on `ModuleName`
  [ty] Bump version of `lsp-types`
  [ty] Refactor to support building constraint sets differently (#23600)
  [ty] Dataclass transform: neither frozen nor non-frozen (#23366)
  [ty] Add snapshot tests for advanced `invalid-assignment` scenarios (#23581)
  [ty] disallow negative narrowing on SubclassOf types (#23598)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fixes Related to suggested fixes for violations preview Related to preview mode features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Autofix for none-not-at-end-of-union (RUF036)

3 participants