fixhunt

ruff PLR1733: fix changes program behavior

fix-changes-behaviorlabelled safereal · highrelated
Findings / 2026-10-17T21-02-04-500Z-1p30-ruff-PLR1733 / r2-p003ruff 0.17.114 → 11 lines2026-10-17 21:02 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

When d is not a standard dict but a custom mapping with __getitem__ side effects (e.g., logging, state changes), replacing d[k] with v omits those effects.

Program

before
1class Mapping:
2 def __init__(self):
3 self.data = {'a': 1}
4 self.log = []
5 def __getitem__(self, k):
6 self.log.append(k)
7 def items(self):
8 return self.data.items()
9m = Mapping()
10for k, v in m.items():
11 print(m[k]) # triggers __getitem__, logs 'a'
after
1class Mapping:
2 def __init__(self):
3 self.data = {'a': 1}
4 self.log = []
5 def __getitem__(self, k):
6 self.log.append(k)
7 def items(self):
8 return self.data.items()
9m = Mapping()
10for k, v in m.items():
11 print(v) # triggers __getitem__, logs 'a'

Output

stdout beforeexit 0
None
stdout afterexit 0
1
fired
1 before · 0 after
runs
each version twice, same file name, no network

Judge Nemotron 3 Ultra advisory real · high

The rule assumes the iterated object is a plain dict where m[k] is a pure lookup with no side effects. Here m is a custom Mapping whose __getitem__ logs each access; the fix replaces the logged lookup with the already-available value v, silently dropping the log entry and changing the observable behavior.

Explanation Nemotron 3 Super unverified

The original loop iterates over key-value pairs from `m.items()`, then prints `m[k]`, which calls `__getitem__` with each key `'a'`. Since `self.data['a']` is defined as `1`, this should print `1`, but the observed output shows `None`, indicating that `__getitem__` likely returns `None` when accessed via `m[k]` despite logging the key—perhaps due to missing or incorrect return logic in `__getitem__`. However, the fix replaces `m[k]` with `v`, which is the value directly yielded by `items()` (i.e., `1`), bypassing `__getitem__` entirely. Thus, the fix changes behavior because it avoids the side-effect-prone `__getitem__` call, and the original method does not return the expected value, making the rewrite unsafe despite being labeled as such. The label "safe" is incorrect here because the semantic dependency on `__getitem__`’s return value is violated.

Reproduce

file
prog.py (the before program)
minimized program
r2-p003.py
evidence sheet
r2-p003.md
regression fixture
r2-p003.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 avoid redundant mapping access · PLR1733 use unpacked value instead of key lookup · PLR1733 replace dict[key] with value in iteration

Details

program
Custom mapping with __getitem__ side effects
hazard
When d is not a standard dict but a custom mapping with __getitem__ side effects (e.g., logging, state changes), replacing d[k] with v omits those effects.
oracle
stdout before: None stdout after: 1
run
2026-10-17T21-02-04-500Z-1p30-ruff-PLR1733
generated program (14 lines) and the fix's diff
1class Mapping:
2 def __init__(self):
3 self.data = {'a': 1}
4 self.log = []
5 def __getitem__(self, k):
6 self.log.append(k)
7 return self.data[k]
8 def items(self):
9 return self.data.items()
10
11m = Mapping()
12for k, v in m.items():
13 print(m[k]) # triggers __getitem__, logs 'a'
14print('log:', m.log)
--- orig.py	2026-10-17 17:02:26.780251901 -0400
+++ fixed.py	2026-10-17 17:02:26.944253843 -0400
@@ -10,5 +10,5 @@
 
 m = Mapping()
 for k, v in m.items():
-    print(m[k])  # triggers __getitem__, logs 'a'
+    print(v)  # triggers __getitem__, logs 'a'
 print('log:', m.log)
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)
```