fixhunt

ruff PLE1141

labelled unknown
Runs / 2026-10-17T20-58-24-882Z-y6u4-ruff-PLE1141ruff 0.17.12026-10-17 20:58 UTC8 programs × 2 round(s)16 programs, 7 fired, 0 fixed, 0 findings, 0 rejectedmodel cost $0.0055

Analysis Nemotron 3 Super

Flags dictionary unpacking in a for-loop target where .items() is missing.

Fix: Rewrites the for-loop target expression by appending '.items()' to the iterated dictionary variable.

10 hazards
  • If the dictionary key is a tuple (e.g., {(1, 2): 3}), the original loop unpacks the tuple key into the loop variables, but after the fix, .items() yields (key, value) pairs where the key is the tuple and the value is the mapped value, changing what gets assigned to the loop variables and thus altering runtime behavior.
  • If the dictionary is actually a non-dict object that defines __iter__ to yield 2-tuples (e.g., a list of pairs), the fix incorrectly assumes it is a dict and calls .items(), which may not exist, causing an AttributeError at runtime.
  • If the iterated variable is not a simple name but an attribute or subscript (e.g., obj.d or d[key]), the fix naively appends '.items()' resulting in invalid syntax like obj.d.items() or d[key].items() which may not be intended or may produce different results due to evaluation timing or side effects.
  • If the loop variable appears in a lambda or nested function within the loop body that captures the loop variable, changing what the loop variable refers to (from key to (key, value) pair) alters the closure semantics and may affect behavior if the captured variable is used later.
  • If the dictionary expression has side effects (e.g., a function call that returns a dict and mutates state), the original code evaluates it once per iteration check, but the fix still evaluates it once; however, if the fix were incorrectly applied multiple times due to faulty tooling, it could cause multiple evaluations, though Ruff's fix applies once.
  • If the code relies on the iterator being exhausted in a certain way (e.g., checking len(d) after the loop), the behavior differs because iterating over d yields keys while iterating over d.items() yields items, potentially exhausting a different view if the dict is mutated during iteration (though both are unsafe with mutation).
  • If the dictionary is a subclass that overrides __iter__ to return something other than keys (e.g., a custom mapping that yields values or pairs), the fix incorrectly assumes standard dict behavior and may break intended iteration logic.
  • If the loop target uses star unpacking (e.g., for *rest in d:) and the dictionary keys are not iterable or have unexpected structure, the fix may cause TypeError during unpacking of .items() output where the original succeeded due to different yielded values.
  • If the loop is inside a try-except block catching ValueError from unpacking, the fix may prevent the exception from being raised (if keys are 2-tuples) or cause a different exception (if keys are not 2-tuples), altering error handling flow.
  • If the dictionary variable is deleted or reassigned within the loop body, the fix does not change that behavior, but the timing of when .items() is called (at loop start) vs. the original iter() call (also at loop start) is equivalent; however, if the variable is rebound, the fix uses the new value's .items() which may differ from the original intent if the rebinding was meant to affect iteration.

Programs

idresulttitlehazard
r1-p001fired, no fixasync for with .items() fixasync def/await ordering, async generators
r1-p002fired, no fixtruthiness in loop conditiontruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r1-p003fired, no fixiterator exhaustion and len() after loopIf the code relies on the iterator being exhausted in a certain way...
r1-p004silent prediction offgenerator vs list consumptiongenerators vs lists, iterators consumed once, lazy vs eager evaluation
r1-p005silent prediction offevaluation order and side effects in targetevaluation order and side effects when expressions are merged, duplicated or deleted
r1-p006fired, no fixattribute/subscript target invalid syntaxIf the iterated variable is not a simple name but an attribute or subscript...
r1-p007fired, no fixnumeric coercion in loop bodyint/float/bool coercion, Decimal, division and rounding
r1-p008fired, no fixrebound variable during iterationIf the dictionary variable is deleted or reassigned within the loop body...
r2-p001silent prediction offPLE1141 Side Effects in Dict ExpressionIf the dictionary expression has side effects (e.g., a function call that returns a dict and mutates state), the original code evaluates it once per iteration check, but the fix still evaluates it onc
r2-p002fired, no fixPLE1141 Defaults Evaluated Oncekeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p003silent prediction offPLE1141 Tuple Key Unpacking ChangedIf the dictionary key is a tuple (e.g., {(1, 2): 3}), the original loop unpacks the tuple key into the loop variables, but after the fix, .items() yields (key, value) pairs where the key is the tuple
r2-p004silent prediction offPLE1141 Non-Dict with __iter__ Yielding 2-TuplesIf the dictionary is actually a non-dict object that defines __iter__ to yield 2-tuples (e.g., a list of pairs), the fix incorrectly assumes it is a dict and calls .items(), which may not exist, causi
r2-p005silent prediction offPLE1141 Custom Mapping Overriding __iter__If the dictionary is a subclass that overrides __iter__ to return something other than keys (e.g., a custom mapping that yields values or pairs), the fix incorrectly assumes standard dict behavior and
r2-p006silentPLE1141 Bytes vs Str Encodingbytes vs str, encoding and newline handling, text vs binary modes
r2-p007silentPLE1141 Star-Imports and Module Side Effectsstar-imports, module-level side effects, __all__
r2-p008silent prediction offPLE1141 Evaluation Order and Side Effects in Targetevaluation order and side effects when expressions are merged, duplicated or deleted

Sources

r1-p001 · async for with .items() fix
1async def main():
2 d = {'a': 1}
3 result = []
4 # Rule triggers here: for k, v in d: -> should become for k, v in d.items():
5 async for k, v in d:
6 result.append((k, v))
7 await asyncio.sleep(0)
8 print('result:', result)
9asyncio.run(main())
r1-p002 · truthiness in loop condition
1class DictLike:
2 def __init__(self, d):
3 self.d = d
4 self.call_count = 0
5 def __aiter__(self):
6 return self
7 async def __anext__(self):
8 if self.call_count >= 2:
9 raise StopAsyncIteration
10 self.call_count += 1
11 return list(self.d.items())[self.call_count - 1]
12 def __bool__(self):
13 return len(self.d) > 0
14
15async def test():
16 d = DictLike({'x': 10})
17 result = []
18 # Rule triggers: for k, v in d: (but d is not a dict, it's DictLike)
19 # However, the rule only fires on dict variables — so we make d a dict but wrap access
20 # Instead: use a dict but make __bool__ matter via conditional break
21 pass
22
23# Let's refactor: use real dict but rely on truthiness to control loop entry
24d = {'a': 1}
25if d: # truthiness check
26 result = []
27 for k, v in d: # <- TRIGGER
28 result.append((k, v))
29 if not d: # mutate to falsy
30 d.clear()
31 print('result:', result, 'dict after:', d)
32else:
33 print('skipped')
r1-p003 · iterator exhaustion and len() after loop
1d = {'a': 1, 'b': 2}
2keys_seen = []
3for k, v in d: # <- TRIGGER: iterates over keys, not items
4 keys_seen.append(k)
5 if k == 'a':
6 d['c'] = 3 # mutate during iteration
7print('keys_seen:', keys_seen)
8print('len(d) after loop:', len(d))
9# Expected: keys_seen = ['a'], len(d)=3
10# After fix: for k, v in d.items(): iterates over items, same mutation risk
11# But: first iteration: ('a',1), then dict size changes -> may skip or raise
12# Actually, mutating dict during items() iteration raises RuntimeError
13# So we catch it
14try:
15 d2 = {'a': 1, 'b': 2}
16 keys_seen2 = []
17 for k, v in d2.items(): # what fix would produce
18 keys_seen2.append(k)
19 if k == 'a':
20 d2['c'] = 3
21 print('keys_seen2:', keys_seen2, 'len(d2):', len(d2))
22except RuntimeError as e:
23 print('RuntimeError in fixed version:', type(e).__name__, str(e))
r1-p004 · generator vs list consumption
1def make_dict():
2 return {'a': 1, 'b': 2}
3
4# First loop: consumes iterator
5d = make_dict()
6list1 = list(d) # exhausts key iterator
7print('list(d) first:', list1)
8
9# Second loop: should be empty if we iterated over keys again
10result = []
11for k, v in d: # <- TRIGGER: but d is exhausted! yields nothing
12 result.append((k, v))
13print('for loop result:', result)
14
15# Now test what fix would do: d.items() returns fresh view
16d2 = make_dict()
17list2 = list(d2) # exhaust keys
18print('list(d2) first:', list2)
19result2 = []
20try:
21 for k, v in d2.items(): # fix version: fresh items view
22 result2.append((k, v))
23 print('fix result:', result2)
24except Exception as e:
25 print('fix error:', type(e).__name__, str(e))
list(d) first: ['a', 'b']
r1-p005 · evaluation order and side effects in target
1i = 0
2side_effects = []
3
4def get_dict():
5 side_effects.append(f'dict_call_{i}')
6 global i
7 i += 1
8 return {'a': 1}
9
10# Rule triggers on: for k, v in get_dict():
11# But wait — rule only fires on simple dict variables, not calls!
12# So we need a variable that is assigned from a call with side effect
13# Let's do:
14d = get_dict() # side effect happens here
15print('side_effects after assignment:', side_effects)
16result = []
17for k, v in d: # <- TRIGGER: d is already evaluated
18 result.append((k, v))
19 side_effects.append(f'in_loop_{len(result)}')
20print('result:', result)
21print('side_effects final:', side_effects)
22# Fix would do: for k, v in d.items(): same d, so no difference in eval timing
23# But to show hazard: suppose we had a property that returns dict and has side effect
24class Obj:
25 @property
26 def d(self):
27 side_effects.append('prop_call')
28 return {'x': 99}
29
30o = Obj()
31side_effects.clear()
32i = 0
33# Now: for k, v in o.d: # <- but rule doesn't fire on attribute! skip for now
34# We'll save attribute hazard for #6
35print('skipping — not attribute')
r1-p006 · attribute/subscript target invalid syntax
1class Container:
2 def __init__(self):
3 self.data = {'a': 1, 'b': 2}
4
5c = Container()
6# Rule should NOT fire on for k, v in c.data: because it's an attribute
7# But if we mistakenly think it does — or test near-miss
8# Instead: make a case where variable looks simple but is subscript
9d = [{'x': 1}, {'y': 2}]
10# for k, v in d[0]: # subscript — rule should not fire (not a dict var)
11# So we need to trigger on a simple name that later becomes subscript? No.
12# Let's just demonstrate the bug: if rule fired on attribute, it would break
13# We simulate by showing what happens if someone writes for k, v in c.data:
14# and a tool incorrectly adds .items()
15try:
16 exec("for k, v in c.data.items(): pass") # this is valid
17 print('c.data.items() works')
18except Exception as e:
19 print('error:', e)
20
21# Now show the broken fix: c.data.items() is ok, but what if we had c.data[0]?
22# Not relevant. Instead: focus on the fact that rule does NOT fire on attributes
23# so we output that it stays silent — but hazard is about if it did fire incorrectly
24# So we write a comment and show that the rule does not trigger here
25print('Rule does not trigger on c.data (attribute)')
26# For actual trigger, we need a simple name — so reset
27d_simple = {'a': 1}
28for k, v in d_simple: # <- TRIGGER
29 print('simple trigger works')
c.data.items() works
Rule does not trigger on c.data (attribute)
r1-p007 · numeric coercion in loop body
1from decimal import Decimal
2
3d = {'a': Decimal('1.5'), 'b': 2}
4sum_val = 0
5count = 0
6# Rule triggers: for k, v in d: # iterates over keys
7for k, v in d: # <- TRIGGER
8 # v is the value, but wait: iterating over d gives (key,?) no — gives key only!
9 # So unpacking fails! Wait: for k, v in d: where d is dict -> iterates over keys, each key is string
10 # trying to unpack string into two vars -> ValueError
11 # So we must catch this
12 try:
13 sum_val += v # This line won't run
14 except:
15 pass
16
17# Actually, the loop will fail immediately
18# So we restructure: show that original code fails, fix would work
19print('Testing original: for k, v in d:')
20try:
21 for k, v in d:
22 sum_val += v
23 count += 1
24except ValueError as e:
25 print('Original error:', type(e).__name__, str(e))
26
27print('Testing fixed: for k, v in d.items():')
28try:
29 sum_val = 0
30 count = 0
31 for k, v in d.items():
32 sum_val += v
33 count += 1
34 print('Fixed result: sum=', sum_val, 'count=', count)
35except Exception as e:
36 print('Fixed error:', type(e).__name__, str(e))
r1-p008 · rebound variable during iteration
1d = {'a': 1, 'b': 2}
2result = []
3for k, v in d: # <- TRIGGER: iterates over keys of original d
4 result.append((k, v))
5 if k == 'a':
6 d = {'c': 3} # rebind d
7 # Note: original loop uses iterator from old d, so continues
8 # Fix version: for k, v in d.items() would evaluate d.items() at start
9 # but if we rebind d inside, it doesn't affect the iterator
10 # So same behavior? Yes — both evaluate iterable at loop start
11 # But hazard says: if rebinding was meant to affect iteration, fix uses new value's .items()
12 # Wait: no — both evaluate the iterable once at start.
13 # So to see difference, we need the variable to be used in the iterable expression
14 # Like: for k, v in get_d(): and get_d() returns d — but rule doesn't fire on call
15 # Instead: make the loop target depend on a variable that we rebind, but the rule only fires on simple dict
16 # So we accept that timing is same — but show that rebound happens
17 pass
18print('result:', result)
19print('d after loop:', d)
20# Now show what fix would do: same rebinding, but iterable is d.items() evaluated at start
21d2 = {'a': 1, 'b': 2}
22result2 = []
23for k, v in d2.items(): # what fix would produce
24 result2.append((k, v))
25 if k == 'a':
26 d2 = {'c': 3}
27print('fix result:', result2)
28print('fix d after loop:', d2)
29# Both loops ran only first iteration because rebinding doesn't affect iterator
30# Output same — but we show it anyway
r2-p001 · PLE1141 Side Effects in Dict Expression
1counter = 0
2
3def make_dict():
4 global counter
5 counter += 1
6 return {'a': 1, 'b': 2}
7
8print('Before loop:', counter)
9for k, v in make_dict(): # Rule fires here: unpacking dict without .items()
10 print(f'{k}:{v}', end=' ')
11print()
12print('After loop:', counter)
Before loop: 0
r2-p002 · PLE1141 Defaults Evaluated Once
1def f(a, *, b=print('default') or 0):
2 pass
3
4d = {'x': 1}
5print('Start')
6for k, v in d: # Rule fires: should be d.items()
7 f(k, v=b)
8print('End')
default
Start
r2-p003 · PLE1141 Tuple Key Unpacking Changed
1d = {(1, 2): 'found', (3, 4): 'lost'}
2print('Original:')
3for k, v in d: # Rule fires: unpacks tuple key into k,v
4 print(f'k={k}, v={v}')
5print()
6print('Expected after .items():')
7for item in d.items():
8 print(f'item={item}')
Original:
k=1, v=2
k=3, v=4

Expected after .items():
item=((1, 2), 'found')
item=((3, 4), 'lost')
r2-p004 · PLE1141 Non-Dict with __iter__ Yielding 2-Tuples
1pairs = [('a', 1), ('b', 2)]
2print('Iterating pairs directly:')
3for k, v in pairs: # Rule fires: pairs is list, not dict
4 print(f'{k}->{v}', end=' ')
5print()
6print('Now trying .items() on list:')
7try:
8 for k, v in pairs.items(): # This line would be the fix, but we show error
9 pass
10except AttributeError as e:
11 print(f'AttributeError: {e}')
Iterating pairs directly:
a->1 b->2 
Now trying .items() on list:
AttributeError: 'list' object has no attribute 'items'
r2-p005 · PLE1141 Custom Mapping Overriding __iter__
1class OddDict(dict):
2 def __iter__(self):
3 return super().values().__iter__()
4
5d = OddDict({'a': 10, 'b': 20})
6print('OddDict iteration (yields values):')
7for v in d: # Does NOT fire rule: single var
8 print(v, end=' ')
9print()
10print('Forcing rule to fire with unpacking:')
11for k, v in d: # Rule fires: expects 2-tuple, but __iter__ yields values
12 print(f'k={k}, v={v}')
OddDict iteration (yields values):
10 20 
Forcing rule to fire with unpacking:
r2-p006 · PLE1141 Bytes vs Str Encoding
1data = b'hello\nworld'
2lines = data.split(b'\n')
3print('Line count:', len(lines))
4for i, line in enumerate(lines): # No trigger: iterating list of bytes
5 print(f'{i}: {line.decode()}')
Line count: 2
0: hello
1: world
r2-p007 · PLE1141 Star-Imports and Module Side Effects
1# Simulate module side effect via function call
2state = []
3
4def init():
5 state.append('loaded')
6 return {'x': 1}
7
8init()
9print('State:', state)
10for k, v in init(): # Trigger: function call returning dict
11 print(k, v)
12print('State after:', state)
State: ['loaded']
r2-p008 · PLE1141 Evaluation Order and Side Effects in Target
1ref = [0]
2
3def get_dict():
4 ref[0] += 1
5 return {'a': ref[0], 'b': ref[0]*2}
6
7print('Start ref:', ref[0])
8for k, v in get_dict(): # Rule fires: get_dict called once per iteration? Actually once.
9 print(f'{k}={v}', end=' ')
10 ref[0] = 99 # Side effect in loop body
11print()
12print('Final ref:', ref[0])
Start ref: 0