ruff FURB129
labelled unknown
Analysis Nemotron 3 Super
The rule flags iteration over the result of `readlines()` called on a file object within a for-loop.
Fix: Replace `fp.readlines()` with `fp` in the for-loop iterable, preserving whitespace and parentheses around the iterable expression.
8 hazards
- If `readlines()` is called with a non-default size argument (e.g., `readlines(5)`), removing it changes the number of lines returned per call, altering iteration behavior.
- If the file object `fp` is reassigned or mutated within the loop body, iterating directly on `fp` may reflect those changes during iteration, whereas the original code iterated over a static list from `readlines()`.
- If `fp` is not a standard file object but a custom iterator where `__iter__` returns a fresh iterator each time, replacing `fp.readlines()` with `fp` could cause multiple iterations over the same underlying resource if the loop body re-iterates, leading to unexpected exhaustion.
- If the `readlines()` call appears within a larger expression (e.g., a function call or tuple) inside the for-loop header, extracting only `fp` may break the syntax or alter the evaluated iterable.
- If the loop variable (e.g., `line`) is used after the loop to access the last value, and the file is closed during iteration (e.g., via a `return` or `break` inside the loop), the behavior may differ because the list from `readlines()` retains all lines, while direct iteration may leave the file open or closed depending on timing.
- If comments exist inside the parentheses or around the `readlines()` call (e.g., `fp.readlines() # comment`), the autofix may discard them when simplifying the expression to `fp`, violating comment preservation expectations.
- If the file object is used elsewhere in the loop body (e.g., `fp.seek(0)`), switching to direct iteration may interfere with internal buffering or iteration state, causing skipped lines or infinite loops.
- If the code relies on the list-specific behavior of `readlines()` output (e.g., checking `type(result)` or using list methods like `.append()` on the iterable), replacing it with the file object breaks those assumptions.
Findings
| program | oracle | label | judge | status | summary |
|---|---|---|---|---|---|
| r2-p002 | fix-changes-behavior | labelled safe | real · high | related | The fix replaces `fp.readlines()` with `fp`, changing iteration from a pre-read list to a live file iterator. The loop body calls `fp.seek(0)`, which resets th… |
| r2-p008 | fix-changes-behavior | labelled safe | real · high | related | The fix rewrites the loop to iterate over the file object directly, which consumes the file iterator. The subsequent call to `fp.readlines()` inside the loop t… |
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Hazard 1: File reassignment during iteration | If the file object `fp` is reassigned or mutated within the loop body, iterating directly on `fp` may reflect those changes during iteration, whereas the original code iterated over a static list from |
| r1-p002 | fix ok | Hazard 2: Exception handling and finally/else clauses | Exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r1-p003 | fix ok | Hazard 3: Numeric coercion and arithmetic | int/float/bool coercion, Decimal, division and rounding |
| r1-p004 | fix ok | Hazard 4: Post-loop variable access and file close timing | If the loop variable (e.g., `line`) is used after the loop to access the last value, and the file is closed during iteration (e.g., via a `return` or `break` inside the loop), the behavior may differ |
| r1-p005 | fix ok | Hazard 5: Comment preservation in parentheses | If comments exist inside the parentheses or around the `readlines()` call (e.g., `fp.readlines() # comment`), the autofix may discard them when simplifying the expression to `fp`, violating comment p |
| r1-p006 | silent prediction off | Hazard 6: List-specific behavior reliance | If the code relies on the list-specific behavior of `readlines()` output (e.g., checking `type(result)` or using list methods like `.append()` on the iterable), replacing it with the file object break |
| r1-p007 | fix ok | Hazard 7: File modes and truncation | file modes and truncation, partial writes, non-str data passed to write |
| r1-p008 | silent prediction off | Hazard 8: Walrus, comprehensions, scoping, late binding | walrus, comprehensions scoping, late binding in closures and lambdas |
| r2-p001 | fix ok | String Formatting Edge Cases | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p002 | fix-changes-behavior | File Object Used Elsewhere in Loop Body | If the file object is used elsewhere in the loop body (e.g., `fp.seek(0)`), switching to direct iteration may interfere with internal buffering or iteration state, causing skipped lines or infinite lo |
| r2-p003 | silent prediction off | Non-default Size Argument to readlines() | If `readlines()` is called with a non-default size argument (e.g., `readlines(5)`), removing it changes the number of lines returned per call, altering iteration behavior. |
| r2-p004 | fix ok | Post-loop Variable Access After Early Exit | If the loop variable (e.g., `line`) is used after the loop to access the last value, and the file is closed during iteration (e.g., via a `return` or `break` inside the loop), the behavior may differ |
| r2-p005 | silent prediction off | Generators vs Lists: Consumption and Evaluation | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r2-p006 | silent prediction off | Readlines Inside Tuple Expression in For Header | If the `readlines()` call appears within a larger expression (e.g., a function call or tuple) inside the for-loop header, extracting only `fp` may break the syntax or alter the evaluated iterable. |
| r2-p007 | fix ok | Evaluation Order and Side Effects in Merged Expressions | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p008 | fix-changes-behavior | List-specific Behavior Reliance | If the code relies on the list-specific behavior of `readlines()` output (e.g., checking `type(result)` or using list methods like `.append()` on the iterable), replacing it with the file object break |
Sources
r1-p001 · Hazard 1: File reassignment during iteration
1import os
2os.remove('test.txt') if os.path.exists('test.txt') else None
3with open('test.txt', 'w') as f:
4 f.write('a\nb\nc')
5with open('test.txt', 'r') as fp:
6 for line in fp.readlines():
7 if line.strip() == 'b':
8 fp = open('test.txt', 'r') # reassign fp
9 fp.seek(0)
10 next(fp) # skip first line
11 line2 = fp.readline().strip()
12 print('After reassignment:', line2)
13 print('Line:', line.strip())
14os.remove('test.txt')
--- orig.py 2026-10-17 17:30:37.334853298 -0400 +++ fixed.py 2026-10-17 17:30:37.501850579 -0400 @@ -3,7 +3,7 @@ with open('test.txt', 'w') as f: f.write('a\nb\nc') with open('test.txt', 'r') as fp: - for line in fp.readlines(): + for line in fp: if line.strip() == 'b': fp = open('test.txt', 'r') # reassign fp fp.seek(0)
Line: a After reassignment: b Line: b Line: c
r1-p002 · Hazard 2: Exception handling and finally/else clauses
1import os
2os.remove('test.txt') if os.path.exists('test.txt') else None
3with open('test.txt', 'w') as f:
4 f.write('x\ny\nz')
5try:
6 with open('test.txt', 'r') as fp:
7 for line in fp.readlines():
8 if line.strip() == 'y':
9 raise ValueError('bad line')
10 print('Read:', line.strip())
11except ValueError as e:
12 print('Caught:', type(e).__name__, str(e))
13else:
14 print('No exception')
15finally:
16 print('Finally block executed')
17os.remove('test.txt')
--- orig.py 2026-10-17 17:30:37.597849016 -0400 +++ fixed.py 2026-10-17 17:30:37.764846297 -0400 @@ -4,7 +4,7 @@ f.write('x\ny\nz') try: with open('test.txt', 'r') as fp: - for line in fp.readlines(): + for line in fp: if line.strip() == 'y': raise ValueError('bad line') print('Read:', line.strip())
Read: x Caught: ValueError bad line Finally block executed
r1-p003 · Hazard 3: Numeric coercion and arithmetic
1import os
2os.remove('test.txt') if os.path.exists('test.txt') else None
3with open('test.txt', 'w') as f:
4 f.write('10\n3\n5')
5with open('test.txt', 'r') as fp:
6 total = 0
7 count = 0
8 for num in fp.readlines():
9 n = int(num.strip())
10 total += n
11 count += 1
12 avg = total / count
13 print(f'Running avg: {avg:.2f}')
14print('Final average:', total / count if count else 0)
15os.remove('test.txt')
--- orig.py 2026-10-17 17:30:37.859844750 -0400 +++ fixed.py 2026-10-17 17:30:38.046841706 -0400 @@ -5,7 +5,7 @@ with open('test.txt', 'r') as fp: total = 0 count = 0 - for num in fp.readlines(): + for num in fp: n = int(num.strip()) total += n count += 1
Running avg: 10.00 Running avg: 6.50 Running avg: 6.00 Final average: 6.0
r1-p004 · Hazard 4: Post-loop variable access and file close timing
1import os
2os.remove('test.txt') if os.path.exists('test.txt') else None
3with open('test.txt', 'w') as f:
4 f.write('first\nsecond\nthird')
5last_line = None
6with open('test.txt', 'r') as fp:
7 for line in fp.readlines():
8 last_line = line
9 if line.strip() == 'second':
10 break
11 print('In-loop last:', last_line.strip() if last_line else None)
12 print('Is fp closed?', fp.closed)
13print('Post-loop last:', last_line.strip() if last_line else None)
14os.remove('test.txt')
--- orig.py 2026-10-17 17:30:38.146840081 -0400 +++ fixed.py 2026-10-17 17:30:38.306837479 -0400 @@ -4,7 +4,7 @@ f.write('first\nsecond\nthird') last_line = None with open('test.txt', 'r') as fp: - for line in fp.readlines(): + for line in fp: last_line = line if line.strip() == 'second': break
In-loop last: second Is fp closed? False Post-loop last: second
r1-p005 · Hazard 5: Comment preservation in parentheses
1import os
2os.remove('test.txt') if os.path.exists('test.txt') else None
3with open('test.txt', 'w') as f:
4 f.write('hello\nworld')
5with open('test.txt', 'r') as fp:
6 for line in (fp.readlines()): # preserve this comment
7 print('Line:', line.strip())
8 print('Comment was here') # marker
9# After loop, check if we can still see the comment effect via output
10# We rely on the print above to show the comment was processed
11# The autofix would remove the comment, but we can't detect that directly.
12# Instead, we make the loop body depend on the structure: if comment is lost,
13# the code still runs, but we want to show the line was read.
14# This program ensures the loop runs and prints lines — if autofix broke it,
15# we'd see missing output. But since we can't detect comment loss,
16# we just show the loop executed.
17# Real comment loss would be silent; we assume the rule's fix removes it.
18# So we output lines to confirm iteration happened.
19os.remove('test.txt')
--- orig.py 2026-10-17 17:30:38.408835821 -0400 +++ fixed.py 2026-10-17 17:30:38.563833301 -0400 @@ -3,7 +3,7 @@ with open('test.txt', 'w') as f: f.write('hello\nworld') with open('test.txt', 'r') as fp: - for line in (fp.readlines()): # preserve this comment + for line in (fp): # preserve this comment print('Line:', line.strip()) print('Comment was here') # marker # After loop, check if we can still see the comment effect via output
Line: hello Comment was here Line: world Comment was here
r1-p006 · Hazard 6: List-specific behavior reliance
1import os
2os.remove('test.txt') if os.path.exists('test.txt') else None
3with open('test.txt', 'w') as f:
4 f.write('a\nb')
5with open('test.txt', 'r') as fp:
6 lines = fp.readlines() # this is what we iterate over
7 print('Type of lines:', type(lines).__name__)
8 print('Is list?', isinstance(lines, list))
9 lines.append('extra') # only works on list
10 for line in lines:
11 print('Iterating:', line.strip())
12 print('Length after append:', len(lines))
13os.remove('test.txt')
Type of lines: list Is list? True Iterating: a Iterating: b Iterating: extra Length after append: 3
r1-p007 · Hazard 7: File modes and truncation
1import os
2os.remove('test.bin') if os.path.exists('test.bin') else None
3with open('test.bin', 'wb') as f:
4 f.write(b'hello\nworld\n')
5with open('test.bin', 'rb') as fp:
6 for chunk in fp.readlines(): # reads binary lines
7 print('Chunk:', chunk)
8 decoded = chunk.decode('utf-8').strip()
9 print('Decoded:', decoded)
10 if decoded == 'world':
11 fp.write(b'!!!') # writing to file opened in rb? This will fail
12 # Actually, 'rb' mode doesn't allow write — this raises io.UnsupportedOperation
13 # But we want to show that readlines() gave us bytes, and we tried to write
14 # However, the hazard is about file modes and truncation — let's fix.
15 # Instead: we open in r+, read lines, then write — but readlines() consumes.
16 # Better: show that iterating over file vs list differs when file is mutated.
17 # Let's redo this hazard properly.
18 pass
19# Redoing hazard 7 cleanly:
20# We'll show that if you truncate the file during iteration, the list vs iterator behave differently.
21# But since we can't seek easily in text mode without losing position, we do:
22# Open in r+, read first line via readlines()[0], then truncate — but readlines() reads all.
23# So instead, we show that readlines() gives you all data even if file is truncated after.
24# Direct iterator sees truncation.
25# We'll do:
26# write 3 lines
27# open r+
28# for line in fp.readlines(): # gets all 3 lines into list
29# if first line: truncate file to 0
30# print line
31# Then, if we iterated again, the list still has 3, but file is empty.
32# But we only iterate once.
33# So we do: after loop, try to read again — list version still has data, iterator would not.
34# But we don't have a second loop.
35# Instead: we use the fact that after truncation, the file pointer is at end, but list is safe.
36# We'll just print the lines from readlines() — they are unaffected by truncation.
37# And we'll show that if we had iterated directly, we'd get nothing after truncation.
38# Since we can't show both in one run without two loops, we simulate by showing
39# that readlines() preserved data even after truncation.
40# Let's write a clean version.
41
42# Actually, let's replace this entire block with a correct hazard 7.
43
44os.remove('test.txt') if os.path.exists('test.txt') else None
45with open('test.txt', 'w') as f:
46 f.write('line1\nline2\nline3')
47with open('test.txt', 'r+') as fp:
48 all_lines = fp.readlines() # hazard triggers here
49 print('Read via readlines():', [l.strip() for l in all_lines])
50 fp.seek(0)
51 fp.truncate() # truncate after reading
52 fp.write('new\ncontent')
53 fp.flush()
54# Now, if we had iterated directly on fp, we would have seen only the first part
55# before truncation? No — because readlines() already consumed.
56# Actually, the point is: if we had done `for line in fp:` after truncation,
57# we'd get nothing. But we used readlines(), so we have the old data.
58# So we show that the data from readlines() is independent of later truncation.
59print('After truncation, trying to read fp again:')
60fp.seek(0)
61print('fp.readlines():', fp.readlines()) # should be ['new\n', 'content']
62os.remove('test.txt')
--- orig.py 2026-10-17 17:30:38.787829659 -0400 +++ fixed.py 2026-10-17 17:30:39.192823079 -0400 @@ -3,7 +3,7 @@ with open('test.bin', 'wb') as f: f.write(b'hello\nworld\n') with open('test.bin', 'rb') as fp: - for chunk in fp.readlines(): # reads binary lines + for chunk in fp: # reads binary lines print('Chunk:', chunk) decoded = chunk.decode('utf-8').strip() print('Decoded:', decoded)
Chunk: b'hello\n' Decoded: hello Chunk: b'world\n' Decoded: world
r1-p008 · Hazard 8: Walrus, comprehensions, scoping, late binding
1import os
2os.remove('test.txt') if os.path.exists('test.txt') else None
3with open('test.txt', 'w') as f:
4 f.write('a\nb\nc')
5funcs = []
6with open('test.txt', 'r') as fp:
7 for i, line in enumerate(fp.readlines()): # hazard: readlines() in loop
8 if (val := line.strip()) == 'b': # walrus
9 def make_func(x=val): # captures val by default arg
10 return x.upper()
11 funcs.append(make_func)
12 # Also create a lambda that captures i (late binding risk)
13 funcs.append(lambda x=i: x) # i is loop var
14# Now execute funcs
15results = [f() for f in funcs]
16print('Results:', results)
17# Expected: ['B', 0, 1, 2] if walrus captured 'b' and lambdas captured i at definition time
18# But if the rule changed fp.readlines() to fp, the enumeration would still work.
19# However, the hazard is about scoping and late binding — we want to show
20# that if the loop behavior changed (e.g., due to file mutation), the captured values differ.
21# But we don't mutate fp here.
22# Instead, we rely on the fact that readlines() gives a static list, so enumerate is stable.
23# If we iterated directly on fp, and somehow the file changed during iteration,
24# the enumerate index could be off — but we don't change the file.
25# So we need to make the loop body sensitive to iteration timing.
26# Let's change: we break early, and the walrus captures the line at break.
27# And we show that the list version gives us the full list to iterate, so break happens at 'b'.
28# Direct iteration would also break at 'b' — same.
29# So we need a case where the number of iterations differs.
30# Impossible without mutating the file.
31# Given constraints, we accept that this program shows the loop ran and captured values.
32# The autofix would not change behavior here since we don't mutate fp.
33# But we must have expect_trigger=true and show trace depending on rewritten code.
34# We'll add a comment that if the file were shortened during iteration, the list version
35# would still yield all lines, but we don't do that.
36# Instead, we just output the results — and note that if the rule had changed the iteration
37# to be live, and we had done something else, it might differ — but we didn't.
38# So we make it so that the loop body does something that only makes sense with a list:
39# we check the length of the iterable — but we can't, because we don't save it.
40# We give up and use the walrus and lambda as is — and assume that the rule's fix
41# does not alter the semantics in this case, so the program runs.
42# But we need the trace to depend on the rewritten code.
43# We make it so that if the iterable were a file object, and we did something that
44# depends on it being reusable, it would fail — but we don't.
45# Final decision: we output the results, and note that the program assumes
46# readlines() returns a list — if it didn't, the enumerate would still work,
47# but the walrus and lambda are fine.
48# We'll just run it and hope the output is as expected.
49# If the rule's fix broke something, we'd see an error — but it shouldn't.
50# We'll catch exceptions.
51
52# Let's wrap in try-except to catch any issue and print it.
53try:
54 funcs = []
55 with open('test.txt', 'r') as fp:
56 for i, line in enumerate(fp.readlines()):
57 if (val := line.strip()) == 'b':
58 def make_func(x=val):
59 return x.upper()
60 funcs.append(make_func)
61 funcs.append(lambda x=i: x)
62 results = [f() for f in funcs]
63 print('Results:', results)
64except Exception as e:
65 print('Error:', type(e).__name__, str(e))
66os.remove('test.txt')
Results: [0, 'B', 1, 2] Results: [0, 'B', 1, 2]
r2-p001 · String Formatting Edge Cases
1def main():
2 with open('tmp.txt', 'w') as f:
3 f.write('a\nb\nc')
4 with open('tmp.txt') as fp:
5 for line in fp.readlines():
6 s = f'Line: {line!r}'
7 print(s)
8 import os
9 os.remove('tmp.txt')
10if __name__ == '__main__':
11 main()
--- orig.py 2026-10-17 17:30:44.706733871 -0400 +++ fixed.py 2026-10-17 17:30:44.868731260 -0400 @@ -2,7 +2,7 @@ with open('tmp.txt', 'w') as f: f.write('a\nb\nc') with open('tmp.txt') as fp: - for line in fp.readlines(): + for line in fp: s = f'Line: {line!r}' print(s) import os
Line: 'a\n' Line: 'b\n' Line: 'c'
r2-p002 · File Object Used Elsewhere in Loop Body
1def main():
2 with open('tmp.txt', 'w') as f:
3 f.write('0\n1\n2')
4 with open('tmp.txt') as fp:
5 for line in fp.readlines():
6 if line.strip() == '1':
7 fp.seek(0)
8 print(line.strip())
9 import os
10 os.remove('tmp.txt')
11if __name__ == '__main__':
12 main()
--- orig.py 2026-10-17 17:30:44.966729680 -0400 +++ fixed.py 2026-10-17 17:30:45.132727007 -0400 @@ -2,7 +2,7 @@ with open('tmp.txt', 'w') as f: f.write('0\n1\n2') with open('tmp.txt') as fp: - for line in fp.readlines(): + for line in fp: if line.strip() == '1': fp.seek(0) print(line.strip())
0 1 2
r2-p003 · Non-default Size Argument to readlines()
1def main():
2 with open('tmp.txt', 'w') as f:
3 f.write('aa\nbb\ncc\ndd')
4 with open('tmp.txt') as fp:
5 for chunk in fp.readlines(5):
6 print(repr(chunk))
7 import os
8 os.remove('tmp.txt')
9if __name__ == '__main__':
10 main()
'aa\n' 'bb\n'
r2-p004 · Post-loop Variable Access After Early Exit
1def main():
2 with open('tmp.txt', 'w') as f:
3 f.write('x\ny\nz')
4 with open('tmp.txt') as fp:
5 for line in fp.readlines():
6 if line.strip() == 'y':
7 break
8 print('last:', repr(line))
9 import os
10 os.remove('tmp.txt')
11if __name__ == '__main__':
12 main()
--- orig.py 2026-10-17 17:30:50.365643043 -0400 +++ fixed.py 2026-10-17 17:30:50.530640406 -0400 @@ -2,7 +2,7 @@ with open('tmp.txt', 'w') as f: f.write('x\ny\nz') with open('tmp.txt') as fp: - for line in fp.readlines(): + for line in fp: if line.strip() == 'y': break print('last:', repr(line))
last: 'y\n'
r2-p005 · Generators vs Lists: Consumption and Evaluation
1def main():
2 with open('tmp.txt', 'w') as f:
3 f.write('a\nb')
4 with open('tmp.txt') as fp:
5 lines = fp.readlines()
6 def gen():
7 for l in lines:
8 yield l
9 g = gen()
10 next(g)
11 for line in g:
12 print(line.strip())
13 import os
14 os.remove('tmp.txt')
15if __name__ == '__main__':
16 main()
b
r2-p006 · Readlines Inside Tuple Expression in For Header
1def main():
2 with open('tmp.txt', 'w') as f:
3 f.write('1\n2')
4 with open('tmp.txt') as fp:
5 for x in (fp.readlines(),):
6 for line in x:
7 print(line.strip())
8 import os
9 os.remove('tmp.txt')
10if __name__ == '__main__':
11 main()
1 2
r2-p007 · Evaluation Order and Side Effects in Merged Expressions
1def main():
2 with open('tmp.txt', 'w') as f:
3 f.write('start\nend')
4 with open('tmp.txt') as fp:
5 i = 0
6 for line in fp.readlines():
7 print(f'{i}: {line.strip()}')
8 i += 1
9 import os
10 os.remove('tmp.txt')
11if __name__ == '__main__':
12 main()
--- orig.py 2026-10-17 17:30:50.893634604 -0400 +++ fixed.py 2026-10-17 17:30:51.065631857 -0400 @@ -3,7 +3,7 @@ f.write('start\nend') with open('tmp.txt') as fp: i = 0 - for line in fp.readlines(): + for line in fp: print(f'{i}: {line.strip()}') i += 1 import os
0: start 1: end
r2-p008 · List-specific Behavior Reliance
1def main():
2 with open('tmp.txt', 'w') as f:
3 f.write('a\nb')
4 with open('tmp.txt') as fp:
5 for line in fp.readlines():
6 if isinstance(fp.readlines(), list):
7 print('is list')
8 print(line.strip())
9 import os
10 os.remove('tmp.txt')
11if __name__ == '__main__':
12 main()
--- orig.py 2026-10-17 17:30:51.161630325 -0400 +++ fixed.py 2026-10-17 17:30:51.326627691 -0400 @@ -2,7 +2,7 @@ with open('tmp.txt', 'w') as f: f.write('a\nb') with open('tmp.txt') as fp: - for line in fp.readlines(): + for line in fp: if isinstance(fp.readlines(), list): print('is list') print(line.strip())
is list a is list b