Skip to content

RUF036 fix is unsafe and introduces syntax errors #23763

@dscorbett

Description

@dscorbett

Summary

The fix for none-not-at-end-of-union (RUF036) should in general be unsafe because it can change the program’s behavior. Example:

$ cat >ruf036_1.py <<'# EOF'
class C:
    def __or__(self, other): return False
    def __ror__(self, other): return True
print(None | C())

from typing import Literal
class M(type):
    def __or__(self, other): return Literal[False]
    def __ror__(self, other): return Literal[True]
class C(metaclass=M): pass
x: None | C
print(__annotate__(1)["x"])
# EOF

$ python3.14 ruf036_1.py
True
typing.Literal[True]

$ ruff --isolated check --select RUF036 --preview ruf036_1.py --fix
Found 2 errors (2 fixed, 0 remaining).

$ python3.14 ruf036_1.py
False
typing.Literal[False]

It can still be safe for standard-library types because they behave as expected with |. I think it is worth a special case for such types because they are common in annotations and it would be nice for the fix to stay safe when possible.

The fix can introduce a syntax error by concatenating tokens. Spaces should be inserted where appropriate. This doesn’t happen in type expressions, but it happens in general, and it might happen in type expressions someday if anything like PEP 827 is accepted. Example:

$ cat >ruf036_2.py <<'# EOF'
print(None | (int)and 2)
print(2 or(None) | int)
# EOF

$ python ruf036_2.py
2
2

$ ruff --isolated check --select RUF036 --preview ruf036_2.py --fix

error: Fix introduced a syntax error. Reverting all changes.

This indicates a bug in Ruff. If you could open an issue at:

    https://github.com/astral-sh/ruff/issues/new?title=%5BFix%20error%5D

...quoting the contents of `ruf036_2.py`, the rule codes RUF036, along with the `pyproject.toml` settings and executed command, we'd be very appreciative!

RUF036 `None` not at the end of the type union.
 --> ruf036_2.py:1:7
  |
1 | print(None | (int)and 2)
  |       ^^^^^^^^^^^^
2 | print(2 or(None) | int)
  |
help: Move `None` to the end of the type union
  - print(None | (int)and 2)
1 + print(int | Noneand 2)
2 | print(2 or(None) | int)

RUF036 `None` not at the end of the type union.
 --> ruf036_2.py:2:11
  |
1 | print(None | (int)and 2)
2 | print(2 or(None) | int)
  |           ^^^^^^^^^^^^
  |
help: Move `None` to the end of the type union
1 | print(None | (int)and 2)
  - print(2 or(None) | int)
2 + print(2 orint | None)

Found 2 errors.
[*] 2 fixable with the `--fix` option.

Version

ruff 0.15.5 (5e4a3d9 2026-03-05)

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingfixesRelated to suggested fixes for violations

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions