ruff FURB122
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | FURB122 Hazard 1: Write Return Value Discarded | 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. |
| r1-p002 | fix ok | FURB122 Hazard 2: File Modes and Truncation | file modes and truncation, partial writes, non-str data passed to write |
| r1-p003 | silent prediction off | FURB122 Hazard 3: Walrus and Closure Scoping | walrus, comprehensions scoping, late binding in closures and lambdas |
| r1-p004 | fix ok | FURB122 Hazard 4: Dict Ordering and Hashing | dict ordering, duplicate keys, __hash__/__eq__ overrides, sets |
| r1-p005 | silent prediction off | FURB122 Hazard 5: Conditional Write Inside Loop | 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. |
| r1-p006 | silent prediction off | FURB122 Hazard 6: Mutable Defaults and Aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p007 | fix ok | FURB122 Hazard 7: Binary File and String Generator Mismatch | For binary files opened in 'wb' mode, using writelines with a generator of strings (instead of bytes) raises TypeError due to type mismatch. |
| r1-p008 | silent prediction off | FURB122 Hazard 8: Loop Variable Mutated After Write | 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. |
| r2-p001 | silent prediction off | Multiple writes - only first replaced | FURB122 Hazard 1: Multiple write statements in loop |
| r2-p002 | silent prediction off | File object reassigned in loop | FURB122 Hazard 2: File object reassignment during iteration |
| r2-p003 | silent prediction off | Truthy/falsy and identity checks | FURB122 Hazard 3: Custom __bool__ and identity vs equality |
| r2-p004 | silent prediction off | Late binding: loop var shadows built-in | FURB122 Hazard 4: Loop variable name conflict with function in write arg |
| r2-p005 | silent prediction off | Keyword-only and defaults evaluated once | FURB122 Hazard 5: Default args, keyword-only, *args/**kwargs |
| r2-p006 | fix ok | Binary file: writelines gets strings instead of bytes | FURB122 Hazard 7: Binary mode with string generator -> TypeError |
| r2-p007 | silent prediction off | Non-str data and partial writes | FURB122 Hazard 8: File modes, truncation, non-str data |
| r2-p008 | silent prediction off | Non-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