fixhunt

ruff FURB122

labelled unknown
Runs / 2026-10-17T21-29-58-650Z-tktt-ruff-FURB122ruff 0.17.12026-10-17 21:29 UTC8 programs × 2 round(s)16 programs, 4 fired, 4 fixed, 0 findings, 0 rejectedmodel cost $0.0037

Analysis Nemotron 3 Super

Flags loops calling IOBase.write inside a for-loop body where each iteration writes a value derived solely from the loop variable.

Fix: Replace the for-loop with a single call to writelines using a generator expression that yields the same values as the original write calls, preserving the loop variable usage.

10 hazards
  • The rewrite may drop comments placed inside the for-loop body, as they are not preserved in the generator expression.
  • If the loop variable is mutated after the write call (e.g., f.write(x); x = None), the rewritten version loses access to the mutated value since it only captures the original iteration value.
  • When the write call is guarded by a condition inside the loop (e.g., if ok: f.write(x)), the fix incorrectly writes all items unconditionally, altering output.
  • If the loop contains multiple write statements (e.g., f.write(a); f.write(b)), the fix only replaces the first, leaving the second and changing behavior.
  • For binary files opened in 'wb' mode, using writelines with a generator of strings (instead of bytes) raises TypeError due to type mismatch.
  • If the iterable being looped over is modified during iteration (e.g., f.write(x); lines.pop(0)), the rewrite evaluates the iterable upfront via the generator, potentially causing different iteration or errors.
  • When the write call is part of a larger expression (e.g., n = f.write(x)), the fix discards the return value, breaking assignments or checks that relied on it.
  • If the file object is reassigned within the loop (e.g., f.write(x); f = open(...)), the rewrite uses the original file object for all writes, ignoring later reassignments.
  • In nested loops, applying the fix to the inner loop may incorrectly hoist the generator expression to the outer scope, causing name collisions or premature evaluation.
  • If the loop variable has the same name as a function used in the write argument (e.g., f.write(len) where len is the loop var), the rewrite captures the wrong binding due to late-binding closure in the generator.

Programs

idresulttitlehazard
r1-p001silent prediction offFURB122 Hazard 1: Write Return Value DiscardedWhen the write call is part of a larger expression (e.g., n = f.write(x)), the fix discards the return value, breaking assignments or checks that relied on it.
r1-p002fix okFURB122 Hazard 2: File Modes and Truncationfile modes and truncation, partial writes, non-str data passed to write
r1-p003silent prediction offFURB122 Hazard 3: Walrus and Closure Scopingwalrus, comprehensions scoping, late binding in closures and lambdas
r1-p004fix okFURB122 Hazard 4: Dict Ordering and Hashingdict ordering, duplicate keys, __hash__/__eq__ overrides, sets
r1-p005silent prediction offFURB122 Hazard 5: Conditional Write Inside LoopWhen the write call is guarded by a condition inside the loop (e.g., if ok: f.write(x)), the fix incorrectly writes all items unconditionally, altering output.
r1-p006silent prediction offFURB122 Hazard 6: Mutable Defaults and Aliasingmutable default arguments and aliasing after a rewrite
r1-p007fix okFURB122 Hazard 7: Binary File and String Generator MismatchFor binary files opened in 'wb' mode, using writelines with a generator of strings (instead of bytes) raises TypeError due to type mismatch.
r1-p008silent prediction offFURB122 Hazard 8: Loop Variable Mutated After WriteIf the loop variable is mutated after the write call (e.g., f.write(x); x = None), the rewritten version loses access to the mutated value since it only captures the original iteration value.
r2-p001silent prediction offMultiple writes - only first replacedFURB122 Hazard 1: Multiple write statements in loop
r2-p002silent prediction offFile object reassigned in loopFURB122 Hazard 2: File object reassignment during iteration
r2-p003silent prediction offTruthy/falsy and identity checksFURB122 Hazard 3: Custom __bool__ and identity vs equality
r2-p004silent prediction offLate binding: loop var shadows built-inFURB122 Hazard 4: Loop variable name conflict with function in write arg
r2-p005silent prediction offKeyword-only and defaults evaluated onceFURB122 Hazard 5: Default args, keyword-only, *args/**kwargs
r2-p006fix okBinary file: writelines gets strings instead of bytesFURB122 Hazard 7: Binary mode with string generator -> TypeError
r2-p007silent prediction offNon-str data and partial writesFURB122 Hazard 8: File modes, truncation, non-str data
r2-p008silent prediction offNon-file write() object (e.g. logging.StreamHandler)FURB122 Hazard 9: Context manager with write() but no close()

Sources

r1-p001 · FURB122 Hazard 1: Write Return Value Discarded
1f = open('tmp.txt', 'w')
2results = []
3for x in [b'a', b'b']:
4 n = f.write(x)
5 results.append(n)
6f.close()
7with open('tmp.txt') as f: content = f.read()
8print('write returns:', results)
9print('file content:', repr(content))
r1-p002 · FURB122 Hazard 2: File Modes and Truncation
1import os
2if os.path.exists('tmp.bin'): os.remove('tmp.bin')
3f = open('tmp.bin', 'wb')
4try:
5 for x in [b'hello', b'world']:
6 f.write(x)
7except Exception as e:
8 print('error during write:', type(e).__name__, str(e))
9f.close()
10with open('tmp.bin', 'rb') as f:
11 data = f.read()
12print('written bytes:', repr(data))
--- orig.py	2026-10-17 17:30:10.161304589 -0400
+++ fixed.py	2026-10-17 17:30:10.326301796 -0400
@@ -2,8 +2,7 @@
 if os.path.exists('tmp.bin'): os.remove('tmp.bin')
 f = open('tmp.bin', 'wb')
 try:
-    for x in [b'hello', b'world']:
-        f.write(x)
+    f.writelines([b'hello', b'world'])
 except Exception as e:
     print('error during write:', type(e).__name__, str(e))
 f.close()
written bytes: b'helloworld'
r1-p003 · FURB122 Hazard 3: Walrus and Closure Scoping
1fs = []
2for i in range(2):
3 f = open(f'tmp{i}.txt', 'w')
4 fs.append(f)
5 for x in [i]:
6 if (n := f.write(str(x))) > 0:
7 pass
8funcs = []
9for f in fs:
10 f.close()
11 def make_reader(filename):
12 return lambda: open(filename).read()
13 funcs.append(make_reader(f'tmp{fs.index(f)}.txt'))
14for i, func in enumerate(funcs):
15 print(f'file {i}:', func())
file 0: 0
file 1: 1
r1-p004 · FURB122 Hazard 4: Dict Ordering and Hashing
1class BadKey:
2 def __init__(self, val): self.val = val
3 def __hash__(self): return hash(self.val)
4 def __eq__(self, other): return self.val == other.val
5f = open('tmp.txt', 'w')
6d = {BadKey(1): 'a', BadKey(2): 'b'}
7for k, v in d.items():
8 f.write(f'{k.val}:{v}\n')
9f.close()
10with open('tmp.txt') as f:
11 lines = f.readlines()
12print('dict output:', [l.strip() for l in lines])
--- orig.py	2026-10-17 17:30:10.555297920 -0400
+++ fixed.py	2026-10-17 17:30:10.728294991 -0400
@@ -4,8 +4,7 @@
     def __eq__(self, other): return self.val == other.val
 f = open('tmp.txt', 'w')
 d = {BadKey(1): 'a', BadKey(2): 'b'}
-for k, v in d.items():
-    f.write(f'{k.val}:{v}\n')
+f.writelines(f'{k.val}:{v}\n' for k, v in d.items())
 f.close()
 with open('tmp.txt') as f:
     lines = f.readlines()
dict output: ['1:a', '2:b']
r1-p005 · FURB122 Hazard 5: Conditional Write Inside Loop
1f = open('tmp.txt', 'w')
2written = []
3for x in [1, 2, 3]:
4 if x % 2 == 1:
5 f.write(str(x))
6 written.append(x)
7f.close()
8with open('tmp.txt') as f:
9 content = f.read()
10print('conditioned writes:', written)
11print('file content:', repr(content))
conditioned writes: [1, 3]
file content: '13'
r1-p006 · FURB122 Hazard 6: Mutable Defaults and Aliasing
1def writer(items, lst=[]):
2 f = open('tmp.txt', 'w')
3 for x in items:
4 f.write(str(x))
5 lst.append(x)
6 f.close()
7 return lst
8first = writer([1, 2])
9second = writer([3, 4])
10print('first call:', first)
11print('second call:', second)
12with open('tmp.txt') as f:
13 print('file content:', repr(f.read()))
first call: [1, 2, 3, 4]
second call: [1, 2, 3, 4]
file content: '34'
r1-p007 · FURB122 Hazard 7: Binary File and String Generator Mismatch
1f = open('tmp.bin', 'wb')
2try:
3 for x in ['a', 'b']:
4 f.write(x.encode()) # correct: bytes
5 f.close()
6 f = open('tmp.bin', 'wb')
7 for x in ['a', 'b']:
8 f.write(x) # error: str in binary mode
9except Exception as e:
10 print('expected error:', type(e).__name__, str(e))
11finally:
12 if not f.closed:
13 f.close()
--- orig.py	2026-10-17 17:30:11.089288883 -0400
+++ fixed.py	2026-10-17 17:30:11.265285908 -0400
@@ -1,11 +1,9 @@
 f = open('tmp.bin', 'wb')
 try:
-    for x in ['a', 'b']:
-        f.write(x.encode())  # correct: bytes
+    f.writelines(x.encode() for x in ['a', 'b'])  # correct: bytes
     f.close()
     f = open('tmp.bin', 'wb')
-    for x in ['a', 'b']:
-        f.write(x)  # error: str in binary mode
+    f.writelines(['a', 'b'])  # error: str in binary mode
 except Exception as e:
     print('expected error:', type(e).__name__, str(e))
 finally:
expected error: TypeError a bytes-like object is required, not 'str'
r1-p008 · FURB122 Hazard 8: Loop Variable Mutated After Write
1f = open('tmp.txt', 'w')
2snapshots = []
3for x in ['a', 'b']:
4 f.write(x)
5 snapshots.append(x)
6 x = 'mutated'
7f.close()
8with open('tmp.txt') as f:
9 content = f.read()
10print('file wrote:', content)
11print('x after write:', snapshots)
file wrote: ab
x after write: ['a', 'b']
r2-p001 · Multiple writes - only first replaced
1f = open('tmp.txt', 'w')
2for i in range(3):
3 f.write(str(i))
4 f.write('_')
5f.close()
6with open('tmp.txt') as f:
7 print(f.read())
0_1_2_
r2-p002 · File object reassigned in loop
1f = open('tmp.txt', 'w')
2try:
3 for i in range(3):
4 f.write(str(i))
5 f = open('tmp2.txt', 'w')
6except UnboundLocalError:
7 pass
8finally:
9 f.close()
10with open('tmp.txt') as f:
11 print('tmp:', f.read())
12with open('tmp2.txt') as f:
13 print('tmp2:', f.read())
tmp: 0
tmp2: 2
r2-p003 · Truthy/falsy and identity checks
1class T:
2 def __init__(self, v): self.v = v
3 def __str__(self): return str(self.v)
4 def __bool__(self): return self.v > 0
5seq = [T(-1), T(0), T(1)]
6f = open('tmp.txt', 'w')
7for x in seq:
8 if x: # depends on __bool__
9 f.write(str(x))
10 f.write('T' if x else 'F')
11f.close()
12print(open('tmp.txt').read())
1T
r2-p004 · Late binding: loop var shadows built-in
1def make_writer():
2 f = open('tmp.txt', 'w')
3 for len in [1, 2, 3]: # len shadows built-in
4 f.write(str(len())) # tries to call int as func
5 f.close()
6try:
7 make_writer()
8except Exception as e:
9 print(type(e).__name__, str(e))
TypeError 'int' object is not callable
r2-p005 · Keyword-only and defaults evaluated once
1def write_all(f, sep=',', *, end='\n'):
2 for x in [1, 2, 3]:
3 f.write(str(x) + sep)
4 f.write(end)
5f = open('tmp.txt', 'w')
6write_all(f, sep='|')
7f.close()
8print(repr(open('tmp.txt').read()))
'1|2|3|\n'
r2-p006 · Binary file: writelines gets strings instead of bytes
1f = open('tmp.bin', 'wb')
2for b in [b'a', b'b', b'c']:
3 f.write(b)
4f.close()
5# Now simulate the broken fix: writelines with str gen on binary file
6try:
7 f = open('tmp2.bin', 'wb')
8 f.writelines(str(i) for i in [1, 2, 3]) # str instead of bytes
9 f.close()
10except Exception as e:
11 print(type(e).__name__, str(e))
--- orig.py	2026-10-17 17:30:17.054188433 -0400
+++ fixed.py	2026-10-17 17:30:17.224185585 -0400
@@ -1,6 +1,5 @@
 f = open('tmp.bin', 'wb')
-for b in [b'a', b'b', b'c']:
-    f.write(b)
+f.writelines([b'a', b'b', b'c'])
 f.close()
 # Now simulate the broken fix: writelines with str gen on binary file
 try:
TypeError a bytes-like object is required, not 'str'
r2-p007 · Non-str data and partial writes
1import io
2f = io.BytesIO()
3for x in [1, 2, 3]:
4 f.write(x.to_bytes(2, 'big')) # write bytes
5f.seek(0)
6print(f.read())
7# Rule would incorrectly try: writelines(int.to_bytes...) -> TypeError
b'\x00\x01\x00\x02\x00\x03'
r2-p008 · Non-file write() object (e.g. logging.StreamHandler)
1import io, logging
2log = logging.getLogger('test')
3handler = logging.StreamHandler(io.StringIO())
4handler.setFormatter(logging.Formatter('%(msg)s'))
5log.addHandler(handler)
6log.setLevel(logging.INFO)
7for msg in ['a', 'b', 'c']:
8 log.info(msg) # calls handler.stream.write
9print(handler.stream.getvalue())
a
b
c