fixhunt

ruff PLR1733

labelled unknown
Runs / 2026-10-17T21-02-04-500Z-1p30-ruff-PLR1733ruff 0.17.12026-10-17 21:02 UTC8 programs × 2 round(s)16 programs, 8 fired, 8 fixed, 4 findings, 0 rejectedmodel cost $0.06

Analysis Nemotron 3 Super

The rule flags dictionary index lookups using the loop variable when iterating over .items() where the corresponding value is already available.

Fix: Replace the dictionary index lookup d[key] with the already available loop variable value that corresponds to the value in the iteration.

10 hazards
  • Rewriting d[k] to v changes behavior if the loop variable v is reassigned later in the loop body, as the original code would re-read the current dictionary value while the fix uses the stale local variable.
  • If the dictionary d is mutated during iteration (e.g., keys added/removed), the original d[k] may raise KeyError or reflect updates, whereas the fixed v refers to the value at iteration start and avoids such errors or misses them.
  • 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.
  • If k or v is used elsewhere in an outer scope and the rewrite alters which variable is referenced due to name collision or shadowing, the fix may incorrectly bind or use the wrong variable.
  • The fix may remove a necessary del or reassignment to d[k] if the lookup was part of a larger expression involving assignment or deletion, altering mutability intent.
  • If the dictionary value accessed via d[k] is a mutable object and the code relies on retrieving the latest reference (e.g., after in-place mutation elsewhere), using v may refer to an outdated object if the dictionary was updated and the loop variable not refreshed.
  • Comments attached to the d[k] expression may be lost if the autofix replaces the entire expression without preserving inline or trailing comments.
  • In nested loops where inner loop variables reuse k or v names, the fix may incorrectly resolve to an inner loop variable instead of the intended outer one, altering semantics.
  • If the dictionary access d[k] occurs in a context where its type is used for overload resolution or generic specialization (e.g., in templating or generic functions), replacing it with v may alter inferred types and break type checking.
  • When d[k] is used in a default argument or lambda capture within the loop, the timing of evaluation shifts from lookup-time to loop-entry-time, potentially capturing different values if the dictionary mutates.

Findings

programoraclelabeljudgestatussummary
r1-p004fix-changes-behaviorlabelled safereal · highrelatedThe fix incorrectly assumes the loop variable 'k' refers to the outer dictionary's key, but the inner loop shadows 'k' with the inner dictionary's key. Replaci…
r1-p007fix-changes-behaviorlabelled safereal · highrelatedThe rule assumes that the value yielded by .items() is identical to the value returned by __getitem__, but the program uses a custom dict subclass where __geti…
r2-p003fix-changes-behaviorlabelled safereal · highrelatedThe 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 a…
r1-p006fix-changes-behaviorlabelled safereal · mediumrelatedThe 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 overr…

Programs

idresulttitlehazard
r1-p001fix okEvaluation Order & Side EffectsReplacing d[k] with v changes evaluation order and duplicates side-effectful expressions.
r1-p002fix okName Collision/Outer Scope ShadowingRewrite uses loop variable 'v' which shadows an outer variable named 'v', changing binding.
r1-p003silent prediction offDel/Reassignment Mutability IntentRewrite removes d[k] from del d[k], breaking deletion intent.
r1-p004fix-changes-behaviorNested Loops Variable CaptureInner loop reuses k,v; fix incorrectly binds to inner loop var instead of outer dict lookup.
r1-p005fix okClass Attributes/Properties/DescriptorsReplace d[k] with v omits descriptor __get__ side effects on class attribute access.
r1-p006fix-changes-behaviorCustom Mapping __getitem__ Side Effectsd[k] invokes logging __getitem__; replacing with v omits the side effect.
r1-p007fix-changes-behaviorAsync Def/Await OrderingIn async context, replacing d[k] with v alters await timing if d[k] is awaitable.
r1-p008fix okPathlib vs Os.Path Behaviord[k] path lookup may trigger filesystem side effects (stat, symlink resolve); v does not.
r2-p001silent prediction offLoop variable reassignment after useRewriting d[k] to v changes behavior if the loop variable v is reassigned later in the loop body, as the original code would re-read the current dictionary value while the fix uses the stale local var
r2-p002silent prediction offLambda capture in default argumentWhen d[k] is used in a default argument or lambda capture within the loop, the timing of evaluation shifts from lookup-time to loop-entry-time, potentially capturing different values if the dictionary
r2-p003fix-changes-behaviorCustom mapping with __getitem__ side effectsWhen 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.
r2-p004silent prediction offMutable default arguments and aliasingmutable default arguments and aliasing after a rewrite
r2-p005silent prediction offInline comment on d[k] expressionComments attached to the d[k] expression may be lost if the autofix replaces the entire expression without preserving inline or trailing comments.
r2-p006silent prediction offGenerator expression inside loopgenerators vs lists, iterators consumed once, lazy vs eager evaluation
r2-p007silent prediction offString formatting with % and .format()string formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p008silent prediction offStar-import and module-level interactionstar-imports, module-level side effects, __all__

Sources

r1-p001 · Evaluation Order & Side Effects
1d = {}
2class Counter:
3 def __init__(self):
4 self.n = 0
5 def __hash__(self):
6 self.n += 1
7 return id(self)
8 def __eq__(self, other):
9 return self is other
10c1 = Counter()
11c2 = Counter()
12d[c1] = 'first'
13d[c2] = 'second'
14trace = []
15for k, v in d.items():
16 trace.append((k.n, v))
17 _ = d[k] # triggers PLR1733: d[k] re-evaluates __hash__ again
18print('trace:', trace)
--- orig.py	2026-10-17 17:02:17.837145789 -0400
+++ fixed.py	2026-10-17 17:02:18.007147810 -0400
@@ -14,5 +14,5 @@
 trace = []
 for k, v in d.items():
     trace.append((k.n, v))
-    _ = d[k]  # triggers PLR1733: d[k] re-evaluates __hash__ again
+    _ = v  # triggers PLR1733: d[k] re-evaluates __hash__ again
 print('trace:', trace)
trace: [(1, 'first'), (1, 'second')]
r1-p002 · Name Collision/Outer Scope Shadowing
1v = 99 # outer scope
2def f(d):
3 trace = []
4 for k, v in d.items(): # inner v shadows outer v
5 trace.append(v) # refers to inner v (dict value)
6 x = d[k] # triggers PLR1733: would become x = v (still inner)
7 trace.append(x)
8 return trace
9print(f({'a': 1, 'b': 2}))
--- orig.py	2026-10-17 17:02:18.108149010 -0400
+++ fixed.py	2026-10-17 17:02:18.275150994 -0400
@@ -3,7 +3,7 @@
     trace = []
     for k, v in d.items():  # inner v shadows outer v
         trace.append(v)     # refers to inner v (dict value)
-        x = d[k]            # triggers PLR1733: would become x = v (still inner)
+        x = v            # triggers PLR1733: would become x = v (still inner)
         trace.append(x)
     return trace
 print(f({'a': 1, 'b': 2}))
[1, 1, 2, 2]
r1-p003 · Del/Reassignment Mutability Intent
1d = {'a': 1, 'b': 2}
2trace = []
3for k, v in d.items():
4 trace.append(len(d))
5 del d[k] # triggers PLR1733: incorrectly would become del v (SyntaxError)
6 trace.append(len(d))
7try:
8 for k, v in d.items():
9 del d[k]
10except Exception as e:
11 trace.append(('error', type(e).__name__, str(e)))
12print('trace:', trace)
r1-p004 · Nested Loops Variable Capture
1outer = {'x': 10}
2middle = {'y': 20}
3inner = [1, 2]
4trace = []
5for k, v in outer.items(): # outer k='x', v=10
6 for k, v in middle.items(): # inner k='y', v=20 (shadows outer)
7 for _ in inner:
8 trace.append((k, v)) # k,y v,20 from middle
9 x = outer[k] # triggers PLR1733: outer['y']?? -> KeyError
10 trace.append(('got', x))
11print('trace:', trace)
--- orig.py	2026-10-17 17:02:18.733156437 -0400
+++ fixed.py	2026-10-17 17:02:19.118161012 -0400
@@ -6,6 +6,6 @@
     for k, v in middle.items():       # inner k='y', v=20 (shadows outer)
         for _ in inner:
             trace.append((k, v))      # k,y v,20 from middle
-            x = outer[k]              # triggers PLR1733: outer['y']?? -> KeyError
+            x = v              # triggers PLR1733: outer['y']?? -> KeyError
             trace.append(('got', x))
 print('trace:', trace)
r1-p005 · Class Attributes/Properties/Descriptors
1class Desc:
2 def __get__(self, obj, objtype):
3 print('Desc.__get__')
4 return 42
5class C:
6 attr = Desc()
7d = {'key': C()}
8trace = []
9for k, v in d.items():
10 trace.append('start')
11 x = d[k].attr # triggers PLR1733: would become v.attr — skips __get__ on d[k]
12 trace.append(x)
13print('trace:', trace)
--- orig.py	2026-10-17 17:02:19.216162176 -0400
+++ fixed.py	2026-10-17 17:02:19.384164171 -0400
@@ -8,6 +8,6 @@
 trace = []
 for k, v in d.items():
     trace.append('start')
-    x = d[k].attr  # triggers PLR1733: would become v.attr — skips __get__ on d[k]
+    x = v.attr  # triggers PLR1733: would become v.attr — skips __get__ on d[k]
     trace.append(x)
 print('trace:', trace)
Desc.__get__
trace: ['start', 42]
r1-p006 · Custom Mapping __getitem__ Side Effects
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)
GET a
trace: [1]
r1-p007 · Async Def/Await Ordering
1async def get_val(k):
2 await asyncio.sleep(0.001)
3 return k
4class AwaitableDict(dict):
5 def __getitem__(self, key):
6 return get_val(key)
7async def main():
8 d = AwaitableDict({'x': 1})
9 trace = []
10 for k, v in d.items():
11 trace.append('before')
12 x = await d[k] # triggers PLR1733: would become await v — but v is int, not awaitable
13 trace.append(('after', x))
14 return trace
15import asyncio
16result = asyncio.run(main())
17print('result:', result)
--- orig.py	2026-10-17 17:02:19.745168460 -0400
+++ fixed.py	2026-10-17 17:02:20.064172249 -0400
@@ -9,7 +9,7 @@
     trace = []
     for k, v in d.items():
         trace.append('before')
-        x = await d[k]  # triggers PLR1733: would become await v — but v is int, not awaitable
+        x = await v  # triggers PLR1733: would become await v — but v is int, not awaitable
         trace.append(('after', x))
     return trace
 import asyncio
result: ['before', ('after', 'x')]
r1-p008 · Pathlib vs Os.Path Behavior
1import os
2os.mkdir('tmpdir')
3os.symlink('tmpdir', 'link_to_tmpdir')
4d = {'link': 'link_to_tmpdir'}
5trace = []
6for k, v in d.items():
7 trace.append('start')
8 p = os.path.realpath(d[k]) # triggers PLR1733: follows symlink via d[k]; fix would use v (string)
9 trace.append(p)
10 try:
11 os.stat(d[k]) # also triggers PLR1733 in same loop
12 except FileNotFoundError as e:
13 trace.append(('stat error', e))
14print('trace:', trace)
15os.unlink('link_to_tmpdir')
16os.rmdir('tmpdir')
--- orig.py	2026-10-17 17:02:20.313175205 -0400
+++ fixed.py	2026-10-17 17:02:20.481177200 -0400
@@ -5,10 +5,10 @@
 trace = []
 for k, v in d.items():
     trace.append('start')
-    p = os.path.realpath(d[k])  # triggers PLR1733: follows symlink via d[k]; fix would use v (string)
+    p = os.path.realpath(v)  # triggers PLR1733: follows symlink via d[k]; fix would use v (string)
     trace.append(p)
     try:
-        os.stat(d[k])           # also triggers PLR1733 in same loop
+        os.stat(v)           # also triggers PLR1733 in same loop
     except FileNotFoundError as e:
         trace.append(('stat error', e))
 print('trace:', trace)
trace: ['start', '<wd>/prog
r2-p001 · Loop variable reassignment after use
1d = {'a': 1}
2for k, v in d.items():
3 v = 99 # reassign loop var
4 print(v) # prints 99 (stale)
99
r2-p002 · Lambda capture in default argument
1d = {'a': 1}
2for k, v in d.items():
3 def f(x=v): # captures v at loop entry
4 return x
5 d['a'] = 2
6 print(f()) # prints 1 (captured old v), not 2
1
r2-p003 · Custom mapping with __getitem__ side effects
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)
1
log: ['a']
r2-p004 · Mutable default arguments and aliasing
1d = {'a': []}
2for k, v in d.items():
3 def g(x=v): # default arg captures v (the list)
4 x.append(1)
5 return x
6 g()
7 print(len(v)) # prints 1: v is the list, mutated by g
1
r2-p005 · Inline comment on d[k] expression
1d = {'a': 1}
2for k, v in d.items():
3 print(v) # this is the value
1
r2-p006 · Generator expression inside loop
1d = {'a': 1, 'b': 2}
2for k, v in d.items():
3 gen = (x for x in [v] if x > 0) # gen uses v
4 print(list(gen))
[1]
[2]
r2-p007 · String formatting with % and .format()
1d = {'a': 42}
2for k, v in d.items():
3 s = '%d' % v # uses v directly
4 print(s)
42
r2-p008 · Star-import and module-level interaction
1import sys
2sys.__dict__['PLUGIN_VALUE'] = 100
3for k, v in sys.__dict__.items():
4 if k == 'PLUGIN_VALUE':
5 print(v) # prints 100
100