fixhunt

ruff PLR1736

labelled unknown
Runs / 2026-10-17T21-04-33-755Z-llwi-ruff-PLR1736ruff 0.17.12026-10-17 21:04 UTC8 programs × 2 round(s)16 programs, 9 fired, 9 fixed, 2 findings, 3 rejectedmodel cost $0.03

Analysis Nemotron 3 Super

Flags redundant indexing of a list when using enumerate where the element is already provided by the loop variables.

Fix: Replace the indexed list access with the loop variable that corresponds to the current element from enumerate.

10 hazards
  • Rewriting `lst[i]` to `x` changes behavior if `lst` is reassigned within the loop body, as the original code uses the updated list while the fix uses the stale loop variable.
  • If `lst` is a property with a setter, accessing `lst[i]` may trigger the setter on each iteration, whereas using `x` avoids this entirely.
  • When `lst` is a mutable local variable that is later rebound in an outer scope, the fix captures the value at loop entry, while the original reflects rebinds.
  • If the list element is a class instance with a custom `__getitem__` that has side effects, replacing `lst[i]` with `x` omits those side effects.
  • In cases where `lst` is a multidimensional list and the index is a tuple (e.g., `lst[i][j]`), the fix cannot be applied because the loop variables do not match the indexing structure.
  • If the loop variable is named identically to the list (e.g., `for lst, lst in enumerate(lst): lst[lst]`), the fix risks incorrect variable capture due to name shadowing.
  • When the indexed access appears in an annotated assignment (e.g., `lst[i]: int = 5`), removing the index breaks the annotation's target.
  • If the indexed expression is part of a larger subscript (e.g., `lst[i][0]`), the fix would incorrectly replace only the outer index, leaving an invalid access.
  • Comments attached to the indexed expression (e.g., `lst[i] # comment`) may be lost if the autofix does not preserve trailing comments during replacement.
  • In a `walrus` expression like `if (tmp := lst[i]) > 0:`, replacing `lst[i]` with `x` alters the assignment target and may affect subsequent uses of `tmp`.

Findings

programoraclelabeljudgestatussummary
r1-p002fix-changes-behaviorlabelled safereal · highduplicateThe fix replaces `lst[i]` with the loop variable `x`, assuming the list is not mutated during iteration. However, the program rebinds `lst` to a new list `[99,…
r2-p004fix-changes-behaviorlabelled safereal · highduplicateThe rule assumes the enumerate target is a list and the loop variables are distinct (index, item), so it replaces lst[lst] with lst. In the program the same na…

Programs

idresulttitlehazard
r1-p001fix okKeyword-only, positional-only, *args/**kwargs forwardingRewriting enumerate index use breaks argument forwarding where positional/keyword semantics differ based on evaluation timing.
r1-p002fix-changes-behaviorMutable local rebound in outer scopeFix captures list value at loop start; original uses rebound value, causing divergence when list is reassigned in outer scope during iteration.
r1-p003invalidMutable default arguments and aliasingRewriting to loop variable avoids re-evaluation of mutable default, but original re-indexes mutated default; aliasing causes shared state exposure.
r1-p004silent prediction offDict ordering, duplicate keys, __hash__/__eq__ overridesRewriting to loop variable avoids re-hashing/re-looking-up dict key; original re-computes hash and may hit different entry if __eq__ is unstable.
r1-p005invalidBytes vs str, encoding and newline handlingRewriting to loop variable avoids re-encoding/decoding; original re-indexes bytes/str and may decode differently if mutating or slicing encoding-sensitive content.
r1-p006fix okString formatting edge cases: %, format, f-strings, repr vs strRewriting to loop variable avoids re-evaluating __str__/__repr__; original re-indexes and may call __str__ again with side effects.
r1-p007fix okClass vs instance attributes, descriptors, properties with side effectsRewriting to loop variable avoids re-triggering __get__; original re-indexes and may invoke descriptor/__getattribute__ again with state change.
r1-p008fix okTruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='Rewriting to loop variable avoids re-evaluating __bool__/__len__; original re-indexes and may invoke __bool__ again, changing control flow.
r2-p001fix okWalrus assignment changes targetIn a walrus expression like `if (tmp := lst[i]) > 0:`, replacing `lst[i]` with `x` alters the assignment target and may affect subsequent uses of `tmp`.
r2-p002silent prediction offClass vs instance attributes, descriptors, properties with side effectsRewriting to loop variable avoids re-triggering __get__; original re-indexes and triggers descriptor each time.
r2-p003silent prediction offProperty setter triggered on each list accessIf `lst` is a property with a setter, accessing `lst[i]` may trigger the setter on each iteration, whereas using `x` avoids this entirely.
r2-p004fix-changes-behaviorVariable capture due to name shadowing in enumerateIf the loop variable is named identically to the list (e.g., `for lst, lst in enumerate(lst): lst[lst]`), the fix risks incorrect variable capture due to name shadowing.
r2-p005fix okExceptions in rewritten code with finally/elseExceptions raised inside the rewritten code, finally/else clauses, exception chaining
r2-p006silent prediction offDict ordering, duplicate keys, __hash__/__eq__ overridesRewriting to loop variable avoids re-hashing/re-looking-up dict key; original re-indexes and may re-hash.
r2-p007invalidCustom __getitem__ with side effectsIf the list element is a class instance with a custom `__getitem__` that has side effects, replacing `lst[i]` with `x` omits those side effects.
r2-p008fix okString formatting edge cases: % , format, f-strings, repr vs strRewriting to loop variable avoids re-evaluating __str__/__repr__; original re-indexes and may trigger __str__ again.

Sources

r1-p001 · Keyword-only, positional-only, *args/**kwargs forwarding
1def f(a, /, b, *, c=print, d=None):
2 c(f'a={a}, b={b}, c={c.__name__ if callable(c) else c}, d={d}')
3
4lst = [1, 2]
5def wrapper(*args, **kwargs):
6 return f(*args, **kwargs)
7
8for i, x in enumerate(lst):
9 # Rule triggers on lst[i] -> should become x
10 wrapper(lst[i], b=x, c=print, d=lst[i])
--- orig.py	2026-10-17 17:04:46.764867461 -0400
+++ fixed.py	2026-10-17 17:04:46.929869320 -0400
@@ -7,4 +7,4 @@
 
 for i, x in enumerate(lst):
     # Rule triggers on lst[i] -> should become x
-    wrapper(lst[i], b=x, c=print, d=lst[i])
+    wrapper(x, b=x, c=print, d=x)
a=1, b=1, c=print, d=1
a=2, b=2, c=print, d=2
r1-p002 · Mutable local rebound in outer scope
1lst = [10, 20]
2for i, x in enumerate(lst):
3 print(f'Before: lst[{i}]={lst[i]}, x={x}')
4 if i == 0:
5 lst = [99, 88] # Rebinding in outer scope
6 print(f'After: lst[{i}]={lst[i]}, x={x}')
--- orig.py	2026-10-17 17:04:47.037870536 -0400
+++ fixed.py	2026-10-17 17:04:47.205872429 -0400
@@ -1,6 +1,6 @@
 lst = [10, 20]
 for i, x in enumerate(lst):
-    print(f'Before: lst[{i}]={lst[i]}, x={x}')
+    print(f'Before: lst[{i}]={x}, x={x}')
     if i == 0:
         lst = [99, 88]  # Rebinding in outer scope
     print(f'After:  lst[{i}]={lst[i]}, x={x}')
Before: lst[0]=10, x=10
After:  lst[0]=99, x=10
Before: lst[1]=88, x=20
After:  lst[1]=88, x=20
r1-p003 · Mutable default arguments and aliasing (generated program does not terminate within the time limit)
1def g(x, lst=[]):
2 lst.append(x)
3 return lst
4
5data = [1, 2]
6results = []
7for idx, val in enumerate(data):
8 # Rule triggers on data[idx] -> should become val
9 results.append(g(val, data)) # Passing data as explicit lst
10print('Results:', results)
11print('Final data:', data)
r1-p004 · Dict ordering, duplicate keys, __hash__/__eq__ overrides
1class BadKey:
2 def __init__(self, val): self.val = val
3 def __hash__(self): return hash(self.val)
4 def __eq__(self, other):
5 # Simulate hash collision drift
6 return self.val == other.val and hasattr(self, '_eq_count') and self._eq_count < 2
7
8d = {}
9k1 = BadKey(1)
10k2 = BadKey(1) # Same hash, potentially eq
11d[k1] = 'first'
12d[k2] = 'second' # Overwrites if __eq__ says equal
13
14items = list(d.items())
15for pos, (k, v) in enumerate(items):
16 # Rule triggers on d[k] -> should become v
17 print(f'd[{k.val}] via index: {d[k]}, via loop: {v}')
d[1] via index: first, via loop: first
d[1] via index: second, via loop: second
r1-p005 · Bytes vs str, encoding and newline handling (generated program does not parse: Traceback (most recent call last):)
1data = b'h\xe9llo
2w\xf6rld' # UTF-8 bytes: hèllo
3wörd
4for ix, b in enumerate(data):
5 # Rule triggers on data[ix] -> should become b
6 char = chr(b) if b < 128 else '?'
7 reconstructed = bytes([data[ix]]) # Original: re-index
8 print(f'idx={ix}: byte={b} ({char}), reconstructed={reconstructed!r}')
r1-p006 · String formatting edge cases: %, format, f-strings, repr vs str
1class Show:
2 def __init__(self, n): self.n = n
3 def __str__(self):
4 print(f'__str__ called for {self.n}')
5 return str(self.n)
6 __repr__ = __str__
7
8items = [Show(1), Show(2)]
9for i, x in enumerate(items):
10 # Rule triggers on items[i] -> should become x
11 s = f'{items[i]}:{x}'
12 print(s)
--- orig.py	2026-10-17 17:04:52.700934264 -0400
+++ fixed.py	2026-10-17 17:04:52.865936120 -0400
@@ -8,5 +8,5 @@
 items = [Show(1), Show(2)]
 for i, x in enumerate(items):
     # Rule triggers on items[i] -> should become x
-    s = f'{items[i]}:{x}'
+    s = f'{x}:{x}'
     print(s)
__str__ called for 1
__str__ called for 1
1:1
__str__ called for 2
__str__ called for 2
2:2
r1-p007 · Class vs instance attributes, descriptors, properties with side effects
1class Desc:
2 def __init__(self, val): self.val = val
3 def __get__(self, obj, objtype):
4 print(f'Desc.__get__ called with val={self.val}')
5 return self.val
6
7class Container:
8 attr = Desc(42)
9 def __init__(self):
10 self.instance_val = 99
11
12c = Container()
13lst = [c, c]
14for i, obj in enumerate(lst):
15 # Rule triggers on lst[i].attr -> should become obj.attr
16 print(f'Access {i}:', end=' ')
17 v = lst[i].attr # Triggers __get__
--- orig.py	2026-10-17 17:04:52.962937210 -0400
+++ fixed.py	2026-10-17 17:04:53.127939065 -0400
@@ -14,4 +14,4 @@
 for i, obj in enumerate(lst):
     # Rule triggers on lst[i].attr -> should become obj.attr
     print(f'Access {i}:', end=' ') 
-    v = lst[i].attr  # Triggers __get__
+    v = obj.attr  # Triggers __get__
Access 0: Desc.__get__ called with val=42
Access 1: Desc.__get__ called with val=42
r1-p008 · Truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
1class Truthy:
2 def __init__(self, flag): self.flag = flag
3 def __bool__(self):
4 print(f'__bool__ called, returning {self.flag}')
5 return self.flag
6
7data = [Truthy(True), Truthy(False)]
8for n, v in enumerate(data):
9 # Rule triggers on data[n] if v > 0 else 0 -> but v is Truthy, not int! Adjusting to match pattern: use v in bool context
10 # Instead: data[n] is used in conditional
11 if data[n]: # Rule should trigger here? No — rule only triggers on direct indexing like lst[i], not in if
12 # So we need to match the hazard pattern: for n, v in enumerate(nums): nums[n] if v > 0 else 0
13 # Let's change to: nums[n] is used in expression
14 pass
15# Redo to match exact pattern: for n, v in enumerate(nums): nums[n] if v > 0 else 0
16# But v is Truthy, not comparable to int. So make v int, but data[n] has side effect
17class NumWithBool:
18 def __init__(self, val): self.val = val
19 def __bool__(self):
20 print(f'__bool__({self.val})')
21 return self.val != 0
22 def __int__(self): return self.val
23
24nums = [NumWithBool(1), NumWithBool(0)]
25for n, v in enumerate(nums):
26 # Rule triggers on nums[n] -> should become v
27 result = nums[n] if v > 0 else 0 # Here, nums[n] is indexed, v is loop var
28 print(f'n={n}, v={v}, result={result}')
--- orig.py	2026-10-17 17:04:53.232940245 -0400
+++ fixed.py	2026-10-17 17:04:53.623944640 -0400
@@ -8,7 +8,7 @@
 for n, v in enumerate(data):
     # Rule triggers on data[n] if v > 0 else 0 -> but v is Truthy, not int! Adjusting to match pattern: use v in bool context
     # Instead: data[n] is used in conditional
-    if data[n]:  # Rule should trigger here? No — rule only triggers on direct indexing like lst[i], not in if
+    if v:  # Rule should trigger here? No — rule only triggers on direct indexing like lst[i], not in if
         # So we need to match the hazard pattern: for n, v in enumerate(nums): nums[n] if v > 0 else 0
         # Let's change to: nums[n] is used in expression
         pass
@@ -24,5 +24,5 @@
 nums = [NumWithBool(1), NumWithBool(0)]
 for n, v in enumerate(nums):
     # Rule triggers on nums[n] -> should become v
-    result = nums[n] if v > 0 else 0  # Here, nums[n] is indexed, v is loop var
+    result = v if v > 0 else 0  # Here, nums[n] is indexed, v is loop var
     print(f'n={n}, v={v}, result={result}')
__bool__ called, returning True
__bool__ called, returning False
r2-p001 · Walrus assignment changes target
1lst = [1, 2, 3]
2for i, x in enumerate(lst):
3 if (tmp := lst[i]) > 0:
4 tmp *= 2
5 else:
6 tmp = 0
7 print(f'i={i}, tmp={tmp}', end=' ')
8print()
--- orig.py	2026-10-17 17:05:01.181029480 -0400
+++ fixed.py	2026-10-17 17:05:01.360031487 -0400
@@ -1,6 +1,6 @@
 lst = [1, 2, 3]
 for i, x in enumerate(lst):
-    if (tmp := lst[i]) > 0:
+    if (tmp := x) > 0:
         tmp *= 2
     else:
         tmp = 0
i=0, tmp=2 i=1, tmp=4 i=2, tmp=6
r2-p002 · Class vs instance attributes, descriptors, properties with side effects
1class Desc:
2 def __get__(self, obj, objtype):
3 print('Desc.__get__')
4 return [10, 20, 30]
5
6class Container:
7 attr = Desc()
8
9c = Container()
10for i, x in enumerate(c.attr):
11 print(f'idx={i}, val={x}, c.attr[{i}]={c.attr[i]}', end=' ')
12print()
Desc.__get__
Desc.__get__
idx=0, val=10, c.attr[0]=10 Desc.__get__
idx=1, val=20, c.attr[1]=20 Desc.__get__
idx=2, val=30, c.attr[2]=30
r2-p003 · Property setter triggered on each list access
1class Bag:
2 def __init__(self):
3 self._data = [5, 6, 7]
4 self.count = 0
5 @property
6 def data(self):
7 return self._data
8 @data.setter
9 def data(self, v):
10 self.count += 1
11 self._data = v
12
13b = Bag()
14for i, x in enumerate(b.data):
15 b.data[i] = x + 1 # triggers setter each time!
16print(f'setter called {b.count} times')
setter called 0 times
r2-p004 · Variable capture due to name shadowing in enumerate
1lst = ['a', 'b', 'c']
2for lst, lst in enumerate(lst): # outer lst shadows inner? actually, first is index, second is element
3 # Rule sees: lst[lst] -> wants to replace with lst (element), but lst is now element, not list
4 # So original: lst[index] -> element at index
5 # Fix: lst (element) -> which is a string, not indexable -> TypeError
6 try:
7 result = lst[lst] # original: indexes the list with the element (string) -> error
8 except Exception as e:
9 print(f'{type(e).__name__}: {e}', end=' ')
10print()
--- orig.py	2026-10-17 17:05:01.732035658 -0400
+++ fixed.py	2026-10-17 17:05:01.900037542 -0400
@@ -4,7 +4,7 @@
     # So original: lst[index] -> element at index
     # Fix: lst (element) -> which is a string, not indexable -> TypeError
     try:
-        result = lst[lst]  # original: indexes the list with the element (string) -> error
+        result = lst  # original: indexes the list with the element (string) -> error
     except Exception as e:
         print(f'{type(e).__name__}: {e}', end=' ')
 print()
TypeError: string indices must be integers, not 'str' TypeError: string indices must be integers, not 'str' TypeError: string indices must be integers, not 'str'
r2-p005 · Exceptions in rewritten code with finally/else
1lst = [1, 2, 0]
2for i, x in enumerate(lst):
3 try:
4 y = 10 / lst[i] # rule fires: lst[i] -> x
5 except ZeroDivisionError as e:
6 print(f'DivByZero at i={i}', end=' ')
7 else:
8 print(f'OK: {y}', end=' ')
9 finally:
10 print('finally', end=' ')
11print()
--- orig.py	2026-10-17 17:05:01.998038641 -0400
+++ fixed.py	2026-10-17 17:05:02.161040467 -0400
@@ -1,7 +1,7 @@
 lst = [1, 2, 0]
 for i, x in enumerate(lst):
     try:
-        y = 10 / lst[i]  # rule fires: lst[i] -> x
+        y = 10 / x  # rule fires: lst[i] -> x
     except ZeroDivisionError as e:
         print(f'DivByZero at i={i}', end=' ')
     else:
OK: 10.0 finally OK: 5.0 finally DivByZero at i=2 finally
r2-p006 · Dict ordering, duplicate keys, __hash__/__eq__ overrides
1class BadKey:
2 def __init__(self, val):
3 self.val = val
4 def __hash__(self):
5 print(f'hash({self.val})')
6 return hash(self.val)
7 def __eq__(self, other):
8 return isinstance(other, BadKey) and self.val == other.val
9
10keys = [BadKey(1), BadKey(2), BadKey(1)] # note: duplicate logical key
11d = {k: k.val*10 for k in keys}
12for i, k in enumerate(keys):
13 _ = d[k] # original: re-hashes k each time
14print('done')
hash(1)
hash(2)
hash(1)
hash(1)
hash(2)
hash(1)
done
r2-p007 · Custom __getitem__ with side effects (generated program does not parse: Traceback (most recent call last):)
1class Wrapper:
2 def __init__(self, value):
3 self.value = value
4 self.access_log = []
5 def __getitem__(self, key):
6 self.access_log.append(key)
7 return self.value + key
8
9w = Wrapper(7)
10 lst = [w] * 3 # three references to same wrapper
11for i, x in enumerate(lst):
12 val = lst[i][0] # rule fires: lst[i] -> x, then x[0]; but fix would lose __getitem__ log
13print(f'access log: {w.access_log}')
r2-p008 · String formatting edge cases: % , format, f-strings, repr vs str
1class Foo:
2 def __init__(self, n):
3 self.n = n
4 def __str__(self):
5 print(f'str({self.n})')
6 return f'str_{self.n}'
7 def __repr__(self):
8 print(f'repr({self.n})')
9 return f'repr_{self.n}'
10
11lst = [Foo(1), Foo(2)]
12for i, x in enumerate(lst):
13 s = f'{lst[i]}' # rule fires: lst[i] -> x
14print('line')
15for i, x in enumerate(lst):
16 s = str(lst[i]) # rule fires again
17print('done')
--- orig.py	2026-10-17 17:05:02.538044693 -0400
+++ fixed.py	2026-10-17 17:05:02.705046565 -0400
@@ -10,8 +10,8 @@
 
 lst = [Foo(1), Foo(2)]
 for i, x in enumerate(lst):
-    s = f'{lst[i]}'  # rule fires: lst[i] -> x
+    s = f'{x}'  # rule fires: lst[i] -> x
 print('line')
 for i, x in enumerate(lst):
-    s = str(lst[i])  # rule fires again
+    s = str(x)  # rule fires again
 print('done')
str(1)
str(2)
line
str(1)
str(2)
done