ruff FURB142
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Keyword-only args forcing list materialization | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r1-p002 | silent prediction off | Aliased set observed by thread-like reader | If the set is aliased elsewhere, the mutation timing affects concurrent readers. |
| r1-p003 | silent | Non-file context manager with buffered state | context managers that are not files, objects with write() but no close() |
| r1-p004 | fix ok | Generator exhausted mid-loop affecting set build | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r1-p005 | fix ok | Set build used in f-string with delayed eval | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r1-p006 | fix ok | Typed set built in loop, annotation triggers at runtime | type annotations evaluated at runtime, typing-only imports |
| r1-p007 | fix ok | Float coercion in set via loop | int/float/bool coercion, Decimal, division and rounding |
| r1-p008 | fix ok | Set arg with side effects duplicated by rewrite | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p001 | fix ok | Exception during add prevents partial state replication | If .add() or .discard() raises an exception, the bulk method may not replicate partial mutation state. |
| r2-p002 | silent prediction off | Walrus assignment in loop condition affects scoping and late binding | walrus, comprehensions scoping, late binding in closures and lambdas |
| r2-p003 | silent prediction off | Aliased set observed by thread-like reader | If the set is aliased elsewhere, the mutation timing affects concurrent readers. |
| r2-p004 | silent prediction off | Comments inside loop block may be discarded by unsafe fix | If comments exist inside the loop block, the fix may discard them when deemed unsafe. |
| r2-p005 | silent prediction off | Set reassigned within loop causes bulk method to operate on wrong instance | If the set is reassigned within the loop, the bulk method operates on the wrong set instance. |
| r2-p006 | silent prediction off | Exceptions inside rewritten code with finally/else clauses | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p007 | fix ok | Generator exhausted mid-loop affects set build due to lazy evaluation | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r2-p008 | silent prediction off | Class attribute vs instance attribute with descriptor side effects | class 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}