Skip to content

[pylint] Avoid false positives in else clause (PLR1733)#25177

Merged
ntBre merged 2 commits into
astral-sh:mainfrom
aconal-com:fix/plr1733-false-positive-else-clause
May 15, 2026
Merged

[pylint] Avoid false positives in else clause (PLR1733)#25177
ntBre merged 2 commits into
astral-sh:mainfrom
aconal-com:fix/plr1733-false-positive-else-clause

Conversation

@aconal-com
Copy link
Copy Markdown
Contributor

Summary

The PLR1733 (unnecessary-dict-index-lookup) rule was incorrectly firing on dictionary accesses inside the else clause of a for loop. The else clause has different control-flow semantics — it only executes when the loop completes without break — and iteration variables from inner loops may not be in scope or may have stale values.

Root cause

In unnecessary_dict_index_lookup(), the stmt_for.orelse was being visited along with stmt_for.body:

visitor.visit_body(&stmt_for.body);
visitor.visit_body(&stmt_for.orelse);  // <-- this was the bug

The else clause of a for loop is not part of the iteration context. Variables bound in the outer loop may be unbound if the loop body never ran, or stale from the last iteration if the loop completed.

Fix

Remove the visitor.visit_body(&stmt_for.orelse) call so the else clause is not checked for unnecessary dict lookups.

Validation

# False positive case (now correctly passes)
cargo run -p ruff -- check test.py --isolated --select PLR1733 --preview
# Before: 1 error (PLR1733 false positive)
# After:  All checks passed

# Existing cases still work
cargo test -p ruff_linter unnecessary_dict_index  # PASS

# Zulip pattern is still detected correctly
cargo run -p ruff -- check test_zulip.py --isolated --select PLR1733 --preview
# Correctly detects: mapped_arrays[mapped_label][i] += value_arrays[label][i]

Before

ruff check would report a false positive on result[res_glob] inside the else clause of the outer for loop, and the autofix would suggest res_priority which is incorrect (the inner loop variable may be unbound/stale).

After

ruff check correctly skips the else clause of the for loop — no false positive.

Fixes #25150

The for-loop's else clause has different control-flow semantics than the
loop body: it executes only when the loop completes without break, and
iteration variables from nested inner loops may not be in scope or may
have stale values.

This was causing a false positive where PLR1733 suggested replacing
result[res_glob] with res_priority inside a for-else, but res_priority
may be unbound if the inner loop never ran, making the suggested fix
incorrect and potentially introducing a NameError.

Fix by removing the visit_body(&stmt_for.orelse) call in
unnecessary_dict_index_lookup(). The else clause of a for loop should
not be treated as part of the iteration context.

Fixes astral-sh#25150.
@astral-sh-bot astral-sh-bot Bot requested a review from ntBre May 15, 2026 09:19
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 15, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Copy link
Copy Markdown
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.

Thanks! I pushed one commit trimming down some of the comments in the tests, but this makes sense to me.

Co-authored-by: Brent Westbrook <[email protected]>
@ntBre ntBre added bug Something isn't working rule Implementing or modifying a lint rule labels May 15, 2026
@ntBre ntBre changed the title fix(PLR1733): do not check for-else clause for unnecessary dict lookups (#25150) [pylint] Avoid false positives in else clause (PLR1733) May 15, 2026
@ntBre ntBre enabled auto-merge (squash) May 15, 2026 15:47
@ntBre ntBre merged commit a63ad97 into astral-sh:main May 15, 2026
44 checks passed
thejchap pushed a commit to thejchap/ruff that referenced this pull request May 23, 2026
…-sh#25177)

## Summary
The `PLR1733` (unnecessary-dict-index-lookup) rule was incorrectly
firing on dictionary accesses inside the `else` clause of a `for` loop.
The `else` clause has different control-flow semantics — it only
executes when the loop completes without `break` — and iteration
variables from inner loops may not be in scope or may have stale values.

## Root cause
In `unnecessary_dict_index_lookup()`, the `stmt_for.orelse` was being
visited along with `stmt_for.body`:

```rust
visitor.visit_body(&stmt_for.body);
visitor.visit_body(&stmt_for.orelse);  // <-- this was the bug
```

The `else` clause of a `for` loop is not part of the iteration context.
Variables bound in the outer loop may be unbound if the loop body never
ran, or stale from the last iteration if the loop completed.

## Fix
Remove the `visitor.visit_body(&stmt_for.orelse)` call so the `else`
clause is not checked for unnecessary dict lookups.

## Validation
```bash
# False positive case (now correctly passes)
cargo run -p ruff -- check test.py --isolated --select PLR1733 --preview
# Before: 1 error (PLR1733 false positive)
# After:  All checks passed

# Existing cases still work
cargo test -p ruff_linter unnecessary_dict_index  # PASS

# Zulip pattern is still detected correctly
cargo run -p ruff -- check test_zulip.py --isolated --select PLR1733 --preview
# Correctly detects: mapped_arrays[mapped_label][i] += value_arrays[label][i]
```

## Before
`ruff check` would report a false positive on `result[res_glob]` inside
the `else` clause of the outer `for` loop, and the autofix would suggest
`res_priority` which is incorrect (the inner loop variable may be
unbound/stale).

## After
`ruff check` correctly skips the `else` clause of the `for` loop — no
false positive.

Fixes astral-sh#25150

---------

Co-authored-by: Aniket Karne <[email protected]>
Co-authored-by: Brent Westbrook <[email protected]>
anishgirianish pushed a commit to anishgirianish/ruff that referenced this pull request May 28, 2026
…-sh#25177)

## Summary
The `PLR1733` (unnecessary-dict-index-lookup) rule was incorrectly
firing on dictionary accesses inside the `else` clause of a `for` loop.
The `else` clause has different control-flow semantics — it only
executes when the loop completes without `break` — and iteration
variables from inner loops may not be in scope or may have stale values.

## Root cause
In `unnecessary_dict_index_lookup()`, the `stmt_for.orelse` was being
visited along with `stmt_for.body`:

```rust
visitor.visit_body(&stmt_for.body);
visitor.visit_body(&stmt_for.orelse);  // <-- this was the bug
```

The `else` clause of a `for` loop is not part of the iteration context.
Variables bound in the outer loop may be unbound if the loop body never
ran, or stale from the last iteration if the loop completed.

## Fix
Remove the `visitor.visit_body(&stmt_for.orelse)` call so the `else`
clause is not checked for unnecessary dict lookups.

## Validation
```bash
# False positive case (now correctly passes)
cargo run -p ruff -- check test.py --isolated --select PLR1733 --preview
# Before: 1 error (PLR1733 false positive)
# After:  All checks passed

# Existing cases still work
cargo test -p ruff_linter unnecessary_dict_index  # PASS

# Zulip pattern is still detected correctly
cargo run -p ruff -- check test_zulip.py --isolated --select PLR1733 --preview
# Correctly detects: mapped_arrays[mapped_label][i] += value_arrays[label][i]
```

## Before
`ruff check` would report a false positive on `result[res_glob]` inside
the `else` clause of the outer `for` loop, and the autofix would suggest
`res_priority` which is incorrect (the inner loop variable may be
unbound/stale).

## After
`ruff check` correctly skips the `else` clause of the `for` loop — no
false positive.

Fixes astral-sh#25150

---------

Co-authored-by: Aniket Karne <[email protected]>
Co-authored-by: Brent Westbrook <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working rule Implementing or modifying a lint rule

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PLR1733 false positive

3 participants