fixhunt

ruff FURB142

labelled unknown
Runs / 2026-10-17T21-32-18-361Z-m0d8-ruff-FURB142ruff 0.17.12026-10-17 21:32 UTC8 programs × 2 round(s)16 programs, 8 fired, 8 fixed, 0 findings, 0 rejectedmodel cost $0.0032

Analysis Nemotron 3 Super

Flags loops that mutate a set via repeated .add()/.discard() calls when they can be replaced with bulk update()/difference_update().

Fix: Replace the loop with a single call to set.update(iterable) for .add() loops or set.difference_update(iterable) for .discard() loops.

8 hazards
  • If the loop body contains additional statements besides .add()/.discard(), removing them changes behavior.
  • If the loop variable is used after the loop, eliminating the loop may alter its final value.
  • If the iterable expression has side effects (e.g., a function call), evaluating it once vs. per iteration changes timing.
  • If the set is aliased elsewhere, the mutation timing affects concurrent readers.
  • If .add() or .discard() raises an exception, the bulk method may not replicate partial mutation state.
  • If the loop includes a break or continue, removing it alters control flow.
  • If the set is reassigned within the loop, the bulk method operates on the wrong set instance.
  • If comments exist inside the loop block, the fix may discard them when deemed unsafe.

Programs

idresulttitlehazard
r1-p001fix okKeyword-only args forcing list materializationkeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r1-p002silent prediction offAliased set observed by thread-like readerIf the set is aliased elsewhere, the mutation timing affects concurrent readers.
r1-p003silentNon-file context manager with buffered statecontext managers that are not files, objects with write() but no close()
r1-p004fix okGenerator exhausted mid-loop affecting set buildgenerators vs lists, iterators consumed once, lazy vs eager evaluation
r1-p005fix okSet build used in f-string with delayed evalstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r1-p006fix okTyped set built in loop, annotation triggers at runtimetype annotations evaluated at runtime, typing-only imports
r1-p007fix okFloat coercion in set via loopint/float/bool coercion, Decimal, division and rounding
r1-p008fix okSet arg with side effects duplicated by rewriteevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p001fix okException during add prevents partial state replicationIf .add() or .discard() raises an exception, the bulk method may not replicate partial mutation state.
r2-p002silent prediction offWalrus assignment in loop condition affects scoping and late bindingwalrus, comprehensions scoping, late binding in closures and lambdas
r2-p003silent prediction offAliased set observed by thread-like readerIf the set is aliased elsewhere, the mutation timing affects concurrent readers.
r2-p004silent prediction offComments inside loop block may be discarded by unsafe fixIf comments exist inside the loop block, the fix may discard them when deemed unsafe.
r2-p005silent prediction offSet reassigned within loop causes bulk method to operate on wrong instanceIf the set is reassigned within the loop, the bulk method operates on the wrong set instance.
r2-p006silent prediction offExceptions inside rewritten code with finally/else clausesexceptions raised inside the rewritten code, finally/else clauses, exception chaining
r2-p007fix okGenerator exhausted mid-loop affects set build due to lazy evaluationgenerators vs lists, iterators consumed once, lazy vs eager evaluation
r2-p008silent prediction offClass attribute vs instance attribute with descriptor side effectsclass attributes vs instance attributes, descriptors, properties with side effects

Sources

r1-p001 · Keyword-only args forcing list materialization
1def process_items(*items, sep=',', _cache=None):
2 if _cache is None:
3 _cache = []
4 result = []
5 for i in items:
6 _cache.append(i)
7 result.append(str(i))
8 return sep.join(result), _cache
9
10seen = set()
11def add_all(seq):
12 nonlocal seen
13 seen = set()
14 for x in seq:
15 seen.add(x)
16 return len(seen)
17
18vals = [1, 2, 2, 3]
19a, cache = process_items(*vals)
20b = add_all(vals)
21print(f'join={a}, cache_len={len(cache)}, set_size={b}')
--- orig.py	2026-10-17 17:32:27.412192102 -0400
+++ fixed.py	2026-10-17 17:32:27.809186553 -0400
@@ -11,8 +11,7 @@
 def add_all(seq):
     nonlocal seen
     seen = set()
-    for x in seq:
-        seen.add(x)
+    seen.update(seq)
     return len(seen)
 
 vals = [1, 2, 2, 3]
r1-p002 · Aliased set observed by thread-like reader
1import threading
2
3shared = {'data': set()}
4refs = [shared['data']]
5
6changed = []
7def watcher():
8 for _ in range(10):
9 changed.append(len(refs[0]))
10 # busy-wait sim
11
12w = threading.Thread(target=watcher)
13w.start()
14
15for i in range(5):
16 shared['data'].add(i)
17
18w.join()
19print('watch:', changed)
20print('final:', len(shared['data']))
watch: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
final: 5
r1-p003 · Non-file context manager with buffered state
1class Buffer:
2 def __init__(self):
3 self.buf = []
4 self.flushed = False
5 def write(self, data):
6 self.buf.append(data)
7 def flush(self):
8 self.flushed = True
9 def __enter__(self):
10 return self
11 def __exit__(self, *exc):
12 pass
13
14log = Buffer()
15with log as l:
16 for n in [1, 2, 3]:
17 l.write(f'num:{n}\n')
18
19print(''.join(log.buf), 'flushed:', log.flushed)
num:1
num:2
num:3
 flushed: False
r1-p004 · Generator exhausted mid-loop affecting set build
1def gen():
2 for i in range(4):
3 yield i
4
5g = gen()
6s = set()
7# first loop consumes gen
8for x in g:
9 s.add(x)
10print('after first loop:', s)
11# second loop sees empty
12for x in g:
13 s.discard(x)
14print('after second loop:', s)
--- orig.py	2026-10-17 17:32:28.278180002 -0400
+++ fixed.py	2026-10-17 17:32:28.435177811 -0400
@@ -5,10 +5,8 @@
 g = gen()
 s = set()
 # first loop consumes gen
-for x in g:
-    s.add(x)
+s.update(g)
 print('after first loop:', s)
 # second loop sees empty
-for x in g:
-    s.discard(x)
+s.difference_update(g)
 print('after second loop:', s)
after first loop: {0, 1, 2, 3}
after second loop: {0, 1, 2, 3}
r1-p005 · Set build used in f-string with delayed eval
1vals = [1, 2, 2, 3]
2s = set()
3for v in vals:
4 s.add(v)
5# f-string evaluates now
6out = f'Set: {sorted(s)}'
7# but we mutate after
8s.add(99)
9print(out)
10print('Now:', sorted(s))
--- orig.py	2026-10-17 17:32:28.537176387 -0400
+++ fixed.py	2026-10-17 17:32:28.709173986 -0400
@@ -1,7 +1,6 @@
 vals = [1, 2, 2, 3]
 s = set()
-for v in vals:
-    s.add(v)
+s.update(vals)
 # f-string evaluates now
 out = f'Set: {sorted(s)}'
 # but we mutate after
Set: [1, 2, 3]
Now: [1, 2, 3, 99]
r1-p006 · Typed set built in loop, annotation triggers at runtime
1from typing import Set
2
3def make_set(n: int) -> Set[int]:
4 s: Set[int] = set()
5 for i in range(n):
6 s.add(i)
7 return s
8
9s1 = make_set(3)
10s2 = make_set(2)
11print('s1:', sorted(s1))
12print('s2:', sorted(s2))
13print('intersection:', sorted(s1 & s2))
--- orig.py	2026-10-17 17:32:28.807172618 -0400
+++ fixed.py	2026-10-17 17:32:28.991170050 -0400
@@ -2,8 +2,7 @@
 
 def make_set(n: int) -> Set[int]:
     s: Set[int] = set()
-    for i in range(n):
-        s.add(i)
+    s.update(range(n))
     return s
 
 s1 = make_set(3)
s1: [0, 1, 2]
s2: [0, 1]
intersection: [0, 1]
r1-p007 · Float coercion in set via loop
1vals = [1, 2.0, 3, 2]
2s = set()
3for v in vals:
4 s.add(v)
5print('mixed:', sorted(s))
6# all same value
7s2 = set()
8for v in [2, 2.0, 2]:
9 s2.add(v)
10print('all 2:', sorted(s2))
11# bool
12sb = set()
13for v in [True, 1, False, 0]:
14 sb.add(v)
15print('bool/int:', sorted(sb))
--- orig.py	2026-10-17 17:32:29.100168530 -0400
+++ fixed.py	2026-10-17 17:32:29.261166286 -0400
@@ -1,15 +1,12 @@
 vals = [1, 2.0, 3, 2]
 s = set()
-for v in vals:
-    s.add(v)
+s.update(vals)
 print('mixed:', sorted(s))
 # all same value
 s2 = set()
-for v in [2, 2.0, 2]:
-    s2.add(v)
+s2.update([2, 2.0, 2])
 print('all 2:', sorted(s2))
 # bool
 sb = set()
-for v in [True, 1, False, 0]:
-    sb.add(v)
+sb.update([True, 1, False, 0])
 print('bool/int:', sorted(sb))
mixed: [1, 2.0, 3]
all 2: [2]
bool/int: [False, True]
r1-p008 · Set arg with side effects duplicated by rewrite
1ref = []
2def log_and_return(x):
3 ref.append(x)
4 return x * 2
5
6s = set()
7for i in range(3):
8 s.add(log_and_return(i))
9print('log:', ref)
10print('set:', sorted(s))
--- orig.py	2026-10-17 17:32:29.363164864 -0400
+++ fixed.py	2026-10-17 17:32:29.528162564 -0400
@@ -4,7 +4,6 @@
     return x * 2
 
 s = set()
-for i in range(3):
-    s.add(log_and_return(i))
+s.update(log_and_return(i) for i in range(3))
 print('log:', ref)
 print('set:', sorted(s))
log: [0, 1, 2]
set: [0, 2, 4]
r2-p001 · Exception during add prevents partial state replication
1class BadAdd:
2 def __init__(self):
3 self.count = 0
4 def __hash__(self):
5 return id(self)
6 def __eq__(self, other):
7 return self is other
8 def __repr__(self):
9 return f'BadAdd({self.count})'
10
11s = set()
12bad_items = [BadAdd(), BadAdd(), BadAdd()]
13bad_items[1].__hash__ = lambda: (_ for _ in ()).throw(ValueError('fail')) # type: ignore
14
15try:
16 for item in bad_items:
17 s.add(item)
18except ValueError as e:
19 print(f'Caught: {e}, s={s}')
--- orig.py	2026-10-17 17:32:34.809089208 -0400
+++ fixed.py	2026-10-17 17:32:34.983086799 -0400
@@ -13,7 +13,6 @@
 bad_items[1].__hash__ = lambda: (_ for _ in ()).throw(ValueError('fail'))  # type: ignore
 
 try:
-    for item in bad_items:
-        s.add(item)
+    s.update(bad_items)
 except ValueError as e:
     print(f'Caught: {e}, s={s}')
r2-p002 · Walrus assignment in loop condition affects scoping and late binding
1funcs = []
2s = set()
3for i in range(3):
4 if (x := i) not in s:
5 s.add(x)
6 funcs.append(lambda: x)
7print([f() for f in funcs]) # Should be [0,1,2] but may change if rewritten
[2, 2, 2]
r2-p003 · Aliased set observed by thread-like reader
1s = {1, 2}
2s_alias = s
3log = []
4def reader():
5 log.append(s_alias.copy())
6
7reader() # initial state
8for x in [3, 4]:
9 s.add(x)
10 reader()
11print('Log:', log)
Log: [{1, 2}, {1, 2, 3}, {1, 2, 3, 4}]
r2-p004 · Comments inside loop block may be discarded by unsafe fix
1s = set()
2for i in range(3):
3 s.add(i) # add i
4 # log this step
5 pass
6print(f'Set after loop: {s}')
Set after loop: {0, 1, 2}
r2-p005 · Set reassigned within loop causes bulk method to operate on wrong instance
1s = set()
2for i in range(3):
3 if i == 1:
4 s = {99}
5 s.add(i)
6print(f'Final s: {s}')
Final s: {1, 2, 99}
r2-p006 · Exceptions inside rewritten code with finally/else clauses
1s = set()
2try:
3 for i in range(3):
4 if i == 1:
5 raise ValueError('bad')
6 s.add(i)
7except ValueError as e:
8 print(f'Caught: {e}')
9else:
10 print('No exception')
11finally:
12 print(f'Finally: s={s}')
Caught: bad
Finally: s={0}
r2-p007 · Generator exhausted mid-loop affects set build due to lazy evaluation
1def gen():
2 for i in range(3):
3 yield i
4
5s = set()
6g = gen()
7for x in g:
8 s.add(x)
9print(f'After first loop: {s}')
10# Reuse generator - now empty
11for x in g:
12 s.add(x)
13print(f'After second loop: {s}')
--- orig.py	2026-10-17 17:32:35.726076524 -0400
+++ fixed.py	2026-10-17 17:32:35.899074132 -0400
@@ -4,10 +4,8 @@
 
 s = set()
 g = gen()
-for x in g:
-    s.add(x)
+s.update(g)
 print(f'After first loop: {s}')
 # Reuse generator - now empty
-for x in g:
-    s.add(x)
+s.update(g)
 print(f'After second loop: {s}')
After first loop: {0, 1, 2}
After second loop: {0, 1, 2}
r2-p008 · Class attribute vs instance attribute with descriptor side effects
1class Descriptor:
2 def __get__(self, obj, owner):
3 print('Descriptor read')
4 return {1, 2, 3}
5 def __set__(self, obj, value):
6 print('Descriptor write')
7
8class Container:
9 s = Descriptor()
10
11c = Container()
12for i in [4, 5]:
13 c.s.add(i) # Triggers descriptor __get__ each time
14print(f'Container.s: {c.s}')
Descriptor read
Descriptor read
Descriptor read
Container.s: {1, 2, 3}