ruff PLE4703
labelled unknown
Analysis Nemotron 3 Super
Flags loops where a set is directly modified via methods like add(), remove(), discard(), pop(), clear(), or update() while iterating over that same set.
Fix: Rewrites the for-loop header from 'for var in set_var:' to 'for var in set_var.copy():', leaving the loop body unchanged.
8 hazards
- The rewrite evaluates set_var.copy() at loop start, so if set_var is reassigned before the loop begins, the original code would iterate over the reassigned set while the fix iterates over a copy of the original set, changing which elements are processed.
- If set_var is a property or item access (e.g., obj.attr or arr[i]), the fix evaluates it once for the copy, whereas the original re-evaluates it on each iteration, potentially iterating over a different object if the property/item changes during looping.
- When the set variable is aliased (another name refers to the same set), modifications via the alias in the loop body affect the original set, but the fix iterates over a snapshot, so the loop may terminate earlier or later than in the original code.
- If the loop body contains a break, continue, or return statement, the fix may execute fewer iterations than the original would have before the error, altering side effects that depend on the number of iterations.
- Any expressions in the loop header beyond the set variable (e.g., in a nested loop or complex target list) are not adjusted by the fix, so malformed headers could become invalid or change meaning after rewriting.
- The fix does not preserve trailing comments on the for line; they are dropped because the entire header is replaced.
- If the set variable is used in a comprehension or lambda within the loop body that captures the variable by reference, the fix’s use of a copy may cause those inner functions to see a different set than the original code would have.
- When the set is empty, the fix still evaluates .copy() (producing an empty set), which is behaviorally equivalent, but if .copy() is overridden in a subclass to have side effects, those side effects occur where none existed before.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Reassignment Before Loop | If `set_var` is reassigned before the loop starts, evaluating `.copy()` too early causes iteration over stale data. |
| r1-p002 | fired, no fix | Non-file Context Manager | Using .copy() inside a non-file cm still locks snapshot; mutation proceeds fine but semantics differ if __exit__ depends on live state. |
| r1-p003 | fired, no fix | Complex Loop Header | Nested loop variables or destructuring targets are not copied, breaking semantics when mutated. |
| r1-p004 | fired, no fix | Lambda Capture in Body | .copy() freezes the set; lambdas inside body capture the copy, not the mutating original. |
| r1-p005 | silent | Text Encoding Edge Case | Writing strings vs bytes differs; .copy() doesn't affect I/O but deterministic trace requires showing state split across modes. |
| r1-p006 | fired, no fix prediction off | Generator Consumption | Iterating a generator once exhausts it; .copy() creates eager snapshot but underlying mutation still affects later reuse if aliased. |
| r1-p007 | invalid | String Formatting | %s, .format(), f-strings all resolve values at runtime; copying the iterated set doesn't change formatting but proves determinism via captured values. |
| r1-p008 | fired, no fix | Mutable Default Aliasing | Loop variable aliased as default arg; .copy() breaks identity causing default to bind to stale copy. |
| r2-p001 | fired, no fix | bytes vs str encoding | text/binary mode confusion affecting set mutation safety during iteration |
| r2-p002 | invalid | type annotations runtime eval | typing-only import causing NameError at runtime when annotation is evaluated |
| r2-p003 | fired, no fix | non-file context manager | object with write() but no close() used in with statement |
| r2-p004 | fired, no fix | exceptions in rewritten code | exception inside loop body triggering finally/else interaction |
| r2-p005 | silent prediction off | custom truthiness | __bool__ and __len__ affecting loop entry and set mutation safety |
| r2-p006 | silent prediction off | reassignment before loop | set_var reassigned before loop, so .copy() uses old value but loop iterates over new set |
| r2-p007 | silent prediction off | async def await ordering | async iteration over set with mutation inside awaited coroutine |
| r2-p008 | silent prediction off | evaluation order side effects | set var appears in multiple places, copy() evaluates once but original may change |
Sources
r1-p001 · Reassignment Before Loop
1S = {1}
2S = {2}
3print('start:', S)
4for x in S:
5 S.add(99)
6 break
7print('end:', sorted(S))
start: {2}
end: [2, 99]r1-p002 · Non-file Context Manager
1class CM:
2 def __enter__(self): return self
3 def __exit__(self, *exc):
4 if hasattr(self, 'seen') and 99 in self.seen:
5 raise ValueError('99 seen')
6with CM() as cm:
7 s = {1, 2}
8 cm.seen = set()
9 for x in s:
10 cm.seen.add(x)
11 s.add(99)
12print('ok')
r1-p003 · Complex Loop Header
1s = {(1, 2), (3, 4)}
2for (a, b) in s:
3 s.clear()
4 print(a + b)
5break
r1-p004 · Lambda Capture in Body
1s = {1}
2fns = []
3for x in s:
4 fns.append(lambda: x in s)
5 s.add(2)
6print([f() for f in fns])
r1-p005 · Text Encoding Edge Case
1data = {'α', 'β'}
2open('tmp.txt', 'w', encoding='utf-8').write(''.join(sorted(data)))
3with open('tmp.txt', 'r', encoding='utf-8') as f:
4 content = f.read()
5import os; os.remove('tmp.txt')
6print(repr(content))
'αβ'
r1-p006 · Generator Consumption
1def gen():
2 yield from {1, 2}
3g = gen()
4s = set(g)
5for x in s:
6 s.discard(x)
7print(len(s))
r1-p007 · String Formatting (generated program does not parse: Traceback (most recent call last):)
1s = {1}
2for x in s:
3 s.add(2)
4 print(f'{x}:{%d}' % len(s))
5break
r1-p008 · Mutable Default Aliasing
1s = {1}
2def f(x, seen=s):
3 seen.add(99)
4 return len(seen)
5for x in s:
6 print(f(x))
7 s.clear()
8break
r2-p001 · bytes vs str encoding
1data = b'hello world'
2s = {ord(c) for c in data.decode()}
3for x in s:
4 try:
5 s.add(len(data))
6 except RuntimeError as e:
7 print('RuntimeError:', type(e).__name__, str(e))
8print('final s:', sorted(s))
r2-p002 · type annotations runtime eval (generated program does not terminate within the time limit)
1from typing import List
2s: List[int] = [1, 2]
3try:
4 for x in s:
5 s.append(3)
6except NameError as e:
7 print('NameError:', type(e).__name__, str(e))
8print('s:', s)
r2-p003 · non-file context manager
1class Buffer:
2 def __init__(self):
3 self.data = []
4 def write(self, x):
5 self.data.append(x)
6buf = Buffer()
7s = {1, 2}
8with buf:
9 for x in s:
10 buf.write(x)
11 s.add(3)
12print('buffer:', buf.data)
13print('set:', s)
r2-p004 · exceptions in rewritten code
1s = {1, 2}
2try:
3 for x in s:
4 if x == 1:
5 raise ValueError('bad')
6 s.discard(x)
7except ValueError as e:
8 print('ValueError:', type(e).__name__, str(e))
9else:
10 print('no exception')
11finally:
12 print('final set:', s)
ValueError: ValueError bad
final set: {1, 2}r2-p005 · custom truthiness
1class SetLike:
2 def __init__(self):
3 self._data = {1, 2}
4 def __iter__(self):
5 return iter(self._data)
6 def __bool__(self):
7 return len(self._data) > 0
8 def __len__(self):
9 return len(self._data)
10 def discard(self, x):
11 self._data.discard(x)
12s = SetLike()
13for x in s:
14 s.discard(x)
15print('remaining:', list(s._data))
r2-p006 · reassignment before loop
1s = {1, 2}
2s = {3, 4}
3for x in s:
4 print('x:', x)
5 s.add(5)
6print('final s:', sorted(s))
x: 3
r2-p007 · async def await ordering
1async def worker(s):
2 for x in s:
3 await asyncio.sleep(0)
4 s.add(x + 10)
5async def main():
6 s = {1, 2}
7 await worker(s)
8 print('result:', sorted(s))
9asyncio.run(main())
r2-p008 · evaluation order side effects
1def get_set():
2 print('get_set called')
3 return {1, 2}
4s = get_set()
5for x in s:
6 print('x:', x)
7 s.clear()
8print('final s:', s)
get_set called x: 1