fixhunt

ruff FURB129

labelled unknown
Runs / 2026-10-17T21-30-17-728Z-bxl4-ruff-FURB129ruff 0.17.12026-10-17 21:30 UTC8 programs × 2 round(s)16 programs, 11 fired, 11 fixed, 2 findings, 0 rejectedmodel cost $0.03

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

programoraclelabeljudgestatussummary
r2-p002fix-changes-behaviorlabelled safereal · highrelatedThe 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-p008fix-changes-behaviorlabelled safereal · highrelatedThe 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

idresulttitlehazard
r1-p001fix okHazard 1: File reassignment during iterationIf 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-p002fix okHazard 2: Exception handling and finally/else clausesExceptions raised inside the rewritten code, finally/else clauses, exception chaining
r1-p003fix okHazard 3: Numeric coercion and arithmeticint/float/bool coercion, Decimal, division and rounding
r1-p004fix okHazard 4: Post-loop variable access and file close timingIf 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-p005fix okHazard 5: Comment preservation in parenthesesIf 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-p006silent prediction offHazard 6: List-specific behavior relianceIf 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-p007fix okHazard 7: File modes and truncationfile modes and truncation, partial writes, non-str data passed to write
r1-p008silent prediction offHazard 8: Walrus, comprehensions, scoping, late bindingwalrus, comprehensions scoping, late binding in closures and lambdas
r2-p001fix okString Formatting Edge Casesstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p002fix-changes-behaviorFile Object Used Elsewhere in Loop BodyIf 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-p003silent prediction offNon-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-p004fix okPost-loop Variable Access After Early ExitIf 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-p005silent prediction offGenerators vs Lists: Consumption and Evaluationgenerators vs lists, iterators consumed once, lazy vs eager evaluation
r2-p006silent prediction offReadlines Inside Tuple Expression in For HeaderIf 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-p007fix okEvaluation Order and Side Effects in Merged Expressionsevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p008fix-changes-behaviorList-specific Behavior RelianceIf 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