fixhunt

ruff PLR1733: fix changes program behavior

fix-changes-behaviorlabelled safereal · mediumrelated
Findings / 2026-10-17T21-02-04-500Z-1p30-ruff-PLR1733 / r1-p006ruff 0.17.110 → 6 lines2026-10-17 21:02 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

d[k] invokes logging __getitem__; replacing with v omits the side effect.

Program

before
1class LogDict(dict):
2 def __getitem__(self, key):
3 print(f'GET {key}')
4d = LogDict({'a': 1})
5for k, v in d.items():
6 _ = d[k] # triggers PLR1733: logs again; fix would omit log
after
1class LogDict(dict):
2 def __getitem__(self, key):
3 print(f'GET {key}')
4d = LogDict({'a': 1})
5for k, v in d.items():
6 _ = v # triggers PLR1733: logs again; fix would omit log

Output

stdout beforeexit 0
GET a
stdout afterexit 0
(no stdout)
fired
1 before · 0 after
runs
each version twice, same file name, no network

Judge Nemotron 3 Ultra advisory real · medium

The program uses a dict subclass that overrides __getitem__ to log each access. The rule's fix replaces d[k] with the loop variable v, which bypasses the overridden method and eliminates the log output. The documentation presents the rewrite as equivalent for ordinary dicts but does not account for subclasses with side effects in __getitem__.

Explanation Nemotron 3 Super unverified

The original loop accesses each value via `d[k]`, which invokes `__getitem__` and prints 'GET a'. The fix replaces this with `_ = v`, using the already retrieved value from iteration without re-accessing the dictionary, so `__getitem__` is never called and nothing is printed. This changes observable behavior because the side effect of logging depends on dictionary access, which the fix eliminates. Thus, the rewrite is not semantics‑preserving—it removes a required side effect even though the variable `v` holds the same value as `d[k]` would return. The fix incorrectly assumes that avoiding re‑access preserves meaning, ignoring that the access itself performs work beyond value retrieval.

Reproduce

file
prog.py (the before program)
minimized program
r1-p006.py
evidence sheet
r1-p006.md
regression fixture
r1-p006.py the minimized program with a header stating the observed change, for the tool's test suite
python3 prog.py
ruff check --preview --isolated --select PLR1733 --fix prog.py
python3 prog.py

Upstream search GitHub issues Tavily Nemotron 3 Super + Ultra

PLR1733 fix · PLR1733 dict items iteration redundant access · ruff PLR1733 unnecessary dictionary lookup during items loop · dict.__getitem__ called on already retrieved value from items()

  • related astral-sh/ruff/issues/25182 same rule PLR1733 but different construct (else-clause access vs loop-body redundant indexing)
  • related astral-sh/ruff/issues/29422 same rule PLR1733 mentioned among other fix bugs but no snippet shows this exact redundant-indexing pattern
  • related astral-sh/ruff/issues/25150 same rule PLR1733 but different construct (else-clause false positive vs loop-body redundant indexing)

Details

program
Custom Mapping __getitem__ Side Effects
hazard
d[k] invokes logging __getitem__; replacing with v omits the side effect.
oracle
stdout before: GET a stdout after:
run
2026-10-17T21-02-04-500Z-1p30-ruff-PLR1733
generated program (10 lines) and the fix's diff
1class LogDict(dict):
2 def __getitem__(self, key):
3 print(f'GET {key}')
4 return super().__getitem__(key)
5d = LogDict({'a': 1})
6trace = []
7for k, v in d.items():
8 trace.append(v)
9 _ = d[k] # triggers PLR1733: logs again; fix would omit log
10print('trace:', trace)
--- orig.py	2026-10-17 17:02:19.488165407 -0400
+++ fixed.py	2026-10-17 17:02:19.650167331 -0400
@@ -6,5 +6,5 @@
 trace = []
 for k, v in d.items():
     trace.append(v)
-    _ = d[k]  # triggers PLR1733: logs again; fix would omit log
+    _ = v  # triggers PLR1733: logs again; fix would omit log
 print('trace:', trace)
rule documentation given to the models
# unnecessary-dict-index-lookup (PLR1733)

Derived from the **Pylint** linter.

Fix is always available.

## What it does
Checks for key-based dict accesses during `.items()` iterations.

## Why is this bad?
When iterating over a dict via `.items()`, the current value is already
available alongside its key. Using the key to look up the value is
unnecessary.

## Example
```python
FRUITS = {"apple": 1, "orange": 10, "berry": 22}

for fruit_name, fruit_count in FRUITS.items():
    print(FRUITS[fruit_name])
```

Use instead:
```python
FRUITS = {"apple": 1, "orange": 10, "berry": 22}

for fruit_name, fruit_count in FRUITS.items():
    print(fruit_count)
```