Skip to content

[ruff] New rule unnecessary-if (RUF050)#24114

Merged
MichaReiser merged 2 commits intoastral-sh:mainfrom
seroperson:i13929-needless-if-rule
Mar 25, 2026
Merged

[ruff] New rule unnecessary-if (RUF050)#24114
MichaReiser merged 2 commits intoastral-sh:mainfrom
seroperson:i13929-needless-if-rule

Conversation

@seroperson
Copy link
Copy Markdown
Contributor

@seroperson seroperson commented Mar 22, 2026

Summary

Implements the unnecessary_if rule (RUF050), which detects bare if statements (no elif/else) where the body contains only pass or ... and the condition is side-effect-free.

The rule complements RUF047 (needless-else): when all branches of an if/else are empty, RUF047 removes the empty else on the first fix pass, and RUF050 removes the remaining empty if on the next pass.

It also addresses #9472: when F401 removes unused imports from conditional blocks (e.g., if TYPE_CHECKING or if sys.version_info guards), the empty if blocks left behind are cleaned up by RUF050, and the now-unused guard imports are then removed by F401 on subsequent fix iterations.

⚠️ It's kind of superset of TC005, but handles not only TYPE_CHECKING guards. Both TC005 and RUF050 triggers on empty TYPE_CHECKING guard and I'm unsure how to be with it. Maybe skip TYPE_CHECKING guards in RUF050?

⚠️ It will "fix" the following the __bool__-like blocks: https://github.com/pandas-dev/pandas/blob/389051d090579550f1a1259855f486d7f7e2159e/pandas/tests/generic/test_generic.py#L147-L150
That's how contains_effect works and it seems a legit limitation, so I'm unsure how we should handle this (and should we at all). Should we make this fix unsafe because of this?

Conditions with side effects (function calls, await, yield, walrus operator) are skipped entirely, so the fix is always safe. If the body contains a comment (inline, own-line, or trailing), the rule also doesn't fire.

Test Plan

  • RUF050.py - main rule test with error cases and non-error.
  • unnecessary_if_and_needless_else - test function, which checks how RUF047 and RUF050 work together.
  • unnecessary_if_and_unused_import - test function, which checks how F401 and RUF050 work together.

Closes #9472

Closes #13929

@astral-sh-bot astral-sh-bot bot requested a review from ntBre March 22, 2026 21:10
@seroperson seroperson force-pushed the i13929-needless-if-rule branch from ed8c2a2 to 4ed7067 Compare March 22, 2026 21:12
@lepotatoclaw

This comment was marked as spam.

@seroperson seroperson force-pushed the i13929-needless-if-rule branch from 4ed7067 to 9336a39 Compare March 24, 2026 21:33
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Mar 24, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+10 -0 violations, +0 -0 fixes in 8 projects; 48 projects unchanged)

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

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

+ providers/amazon/tests/system/amazon/aws/example_bedrock_batch_inference.py:112:9: RUF050 [*] Empty `if` statement
+ shared/configuration/tests/configuration/test_parser.py:591:9: RUF050 [*] Empty `if` statement

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

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

+ superset/db_engine_specs/gsheets.py:518:13: RUF050 [*] Empty `if` statement

binary-husky/gpt_academic (+1 -0 violations, +0 -0 fixes)

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

+ request_llms/embed_models/openai_embed.py:84:1: RUF050 [*] Empty `if` statement

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

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

+ libs/langchain/tests/unit_tests/test_imports.py:39:21: RUF050 [*] Empty `if` statement

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

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

+ pandas/tests/generic/test_generic.py:148:13: RUF050 [*] Empty `if` statement
+ pandas/tests/indexes/test_any_index.py:19:9: RUF050 [*] Empty `if` statement

reflex-dev/reflex (+1 -0 violations, +0 -0 fixes)

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

+ tests/units/vars/test_dep_tracking.py:279:13: RUF050 [*] Empty `if` statement

rotki/rotki (+1 -0 violations, +0 -0 fixes)

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

+ rotkehlchen/icons.py:30:1: RUF050 [*] Empty `if` statement

zulip/zulip (+1 -0 violations, +0 -0 fixes)

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

+ zerver/tests/test_validators.py:34:1: RUF050 [*] Empty `if` statement

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF050 10 10 0 0 0

@seroperson seroperson force-pushed the i13929-needless-if-rule branch 3 times, most recently from 5c57d2b to 63f03f0 Compare March 24, 2026 23:07
@MichaReiser MichaReiser added rule Implementing or modifying a lint rule preview Related to preview mode features labels Mar 25, 2026
Copy link
Copy Markdown
Member

@MichaReiser MichaReiser 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 overall looks great. I've only a few smaller comments

# Trailing comment belonging to body
if True:
pass
# trailing comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you add an example where the if is the only statement within a suite like what we see in https://github.com/pandas-dev/pandas/blob/389051d090579550f1a1259855f486d7f7e2159e/pandas/tests/generic/test_generic.py#L148

with pytest.raises(ValueError, match=msg):
    if obj1:
        pass

Copy link
Copy Markdown
Contributor Author

@seroperson seroperson Mar 25, 2026

Choose a reason for hiding this comment

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

I've added, but this intentionally kind of "wrongly" (specifically for this case) results in:

with pytest.raises(ValueError, match=msg):
    pass

Python's if statement calls obj1.__bool__() to evaluate the condition, and pandas overrides __bool__ on DataFrame to raise ValueError. That's what this check asserts (that you can't do if some_data_frame: checks in your code).

With RUF050 auto-fix this test will fail and I'm not sure what we can do about it (I've mentioned this in PR's description initially).

Shortly, in current implementation the situation is so:

  • We strip if Var: pass entirely.
  • Side-effects like if foo(): pass result in foo().
  • But technically if Var: pass is actually if Var.__bool__(): pass, which has side-effect and logically must result in Var.__bool__(). But that's weird.

So we either should consider this limitation intended and expect users to set # noqa: RUF050 where they expect some side-effect from __bool__() (which is quite very edge case to be honest), or maybe make the whole rule unsafe.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's mention this caveat in the documentation. This should be very uncommon and we can always change it if many users are running into this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let me know when you updated the documentation so that we can merge this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@MichaReiser
Copy link
Copy Markdown
Member

⚠️ It's kind of superset of TC005, but handles not only TYPE_CHECKING guards. Both TC005 and RUF050 triggers on empty TYPE_CHECKING guard and I'm unsure how to be with it. Maybe skip TYPE_CHECKING guards in RUF050?

Skipping TYPE_CHECKING blocks for now seems reasonable. It gives people the option to keep empty TYPE_CHECKING blocks if they prefer so.

@seroperson seroperson force-pushed the i13929-needless-if-rule branch from 63f03f0 to e6ddbd7 Compare March 25, 2026 13:35
@seroperson seroperson force-pushed the i13929-needless-if-rule branch from e6ddbd7 to 2af839c Compare March 25, 2026 13:38
@seroperson seroperson changed the title Implement needless-if (RUF050) Implement unnecessary-if (RUF050) Mar 25, 2026
@seroperson seroperson requested a review from MichaReiser March 25, 2026 13:43
@seroperson seroperson force-pushed the i13929-needless-if-rule branch from 9a62cdb to b5dcd1e Compare March 25, 2026 16:41
Copy link
Copy Markdown
Member

@MichaReiser MichaReiser 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

@MichaReiser MichaReiser merged commit 3a44cce into astral-sh:main Mar 25, 2026
42 checks passed
carljm added a commit that referenced this pull request Mar 25, 2026
* main:
  [ty] make `test-case` a dev-dependency (#24187)
  [ty] implement cycle normalization for more types to prevent too-many-cycle panics (#24061)
  [ty] Silence all diagnostics in unreachable code (#24179)
  [ty] Intern `InferableTypeVars` (#24161)
  Implement unnecessary-if (RUF050) (#24114)
  Recognize `Self` annotation and `self` assignment in SLF001 (#24144)
  Bump the npm version before publish (#24178)
  [ty] Disallow Self in metaclass and static methods (#23231)
  Use trusted publishing for NPM packages (#24171)
  [ty] Respect non-explicitly defined dataclass params (#24170)
  Add RUF072: warn when using  operator on an f-string (#24162)
  [ty] Check return type of generator functions (#24026)
  Implement useless-finally (RUF-072) (#24165)
  [ty] Add test for a dataclass with a default field converter (#24169)
  [ty] Dataclass field converters (#23088)
  [flake8-bandit] Treat sys.executable as trusted input in S603 (#24106)
  [ty] Add support for `typing.Concatenate` (#23689)
  `ASYNC115`: autofix to use full qualified `anyio.lowlevel` import (#24166)
  [ty] Disallow read-only fields in TypedDict updates (#24128)
  Speed up diagnostic rendering (#24146)
@amyreese amyreese changed the title Implement unnecessary-if (RUF050) [ruff] New rule unnecessary-if (RUF050) Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Related to preview mode features rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detect empty conditional statements Lone unused import in if-statement should not get autofixed, or the if-statement should removed

4 participants