fixhunt

ruff FURB129

labelled unknown
Runs / 2026-10-22T17-16-41-266Z-868l-ruff-FURB129ruff 0.17.12026-10-22 17:16 UTC6 programs × 1 round(s)6 programs, 4 fired, 4 fixed, 1 findings, 1 rejectedmodel cost $0.0083

Analysis Nemotron 3 Super

Flags iteration over the result of file.readlines() where direct iteration over the file object is preferred.

Fix: Replaces 'fp.readlines()' with 'fp' in the iterable position of a for loop, preserving whitespace and parentheses around the call but removing the .readlines() attribute access.

10 hazards
  • If the file object has been assigned to a local variable via aliasing (e.g., f = fp; for line in f.readlines()), the fix would incorrectly rewrite only the original name, breaking the iteration.
  • If readlines is overridden in a subclass of the file object (e.g., class MyFile(TextIO): def readlines(self): return [...]), the fix changes behavior by bypassing the override and iterating directly over the file.
  • If the readlines call includes arguments (e.g., fp.readlines(limit)), the fix would drop the argument, altering the number of lines returned and potentially causing truncation or TypeError.
  • If the file object is not actually a file (e.g., a mock object with a readlines property that returns a computed value), the fix would replace the property access with the object itself, changing the iterated value.
  • If the readlines call is part of a larger expression (e.g., for line in fp.readlines() + []), the fix would incorrectly rewrite only the readlines part, leaving an invalid expression (fp + []).
  • If the code relies on readlines returning a list (e.g., for later indexing or len() inside the loop), the fix changes the type from list to _io.TextIOWrapper, breaking such operations.
  • If the file is opened in binary mode and readlines is used to get byte strings, the fix still works but may be misleading if the intent was to process bytes via a custom readlines override.
  • If there are comments attached to the readlines attribute access (e.g., fp.readlines # inline comment), the fix would drop those comments during AST rewrite.
  • If the file object is deleted or reassigned within the loop body, the fix does not affect this, but the original readlines call had already evaluated the list, so the timing of file closure differs.
  • If the iterable is wrapped in a call to reversed(), sorted(), or similar (e.g., for line in reversed(fp.readlines())), the fix would produce reversed(fp), which iterates in reverse but may have different performance or buffering characteristics.

Findings

programoraclelabeljudgestatussummary
r1-p003fix-changes-behaviorlabelled safereal · highrelatedThe rule rewrites `for line in fp.readlines():` to `for line in fp:`, which changes the file position semantics. In the original code, `fp.readlines()` inside …

Programs

idresulttitlehazard
r1-p001silent prediction offMock Object with Custom readlines PropertyIf the file object is not actually a file (e.g., a mock object with a readlines property that returns a computed value), the fix would replace the property access with the object itself, changing the
r1-p002fix okWalrus Assignment in Loop Condition Affecting Scopingwalrus, comprehensions scoping, late binding in closures and lambdas
r1-p003fix-changes-behaviorReliance on readlines Returning a List for Indexing Inside LoopIf the code relies on readlines returning a list (e.g., for later indexing or len() inside the loop), the fix changes the type from list to _io.TextIOWrapper, breaking such operations.
r1-p004fix okBoolean Coercion in Loop Context After Fixint/float/bool coercion, Decimal, division and rounding
r1-p005invalidBinary Mode readlines Returns bytes, Fix Breaks Encoding Assumptionbytes vs str, encoding and newline handling, text vs binary modes
r1-p006fix okType Annotation Evaluated at Runtime Depending on Iterationtype annotations evaluated at runtime, typing-only imports

Sources

r1-p001 · Mock Object with Custom readlines Property
1class MockFile:
2 @property
3 def readlines(self):
4 return ['mocked', 'line']
5
6fp = MockFile()
7trace = []
8for line in fp.readlines():
9 trace.append(line)
10print(''.join(trace))
r1-p002 · Walrus Assignment in Loop Condition Affecting Scoping
1lines = ['a\n', 'b\n', 'c\n']
2with open('tmp.txt', 'w') as f:
3 f.writelines(lines)
4
5with open('tmp.txt') as fp:
6 trace = []
7 while (line := fp.readline()) and len(trace) < 2:
8 trace.append(line.strip())
9 # Reset and trigger rule
10 fp.seek(0)
11 for x in fp.readlines(): # Rule triggers here
12 trace.append(x.strip())
13 if len(trace) >= 4:
14 break
15print('|'.join(trace))
--- orig.py	2026-10-22 13:16:52.515694669 -0400
+++ fixed.py	2026-10-22 13:16:55.858746898 -0400
@@ -8,7 +8,7 @@
         trace.append(line.strip())
     # Reset and trigger rule
     fp.seek(0)
-    for x in fp.readlines():  # Rule triggers here
+    for x in fp:  # Rule triggers here
         trace.append(x.strip())
         if len(trace) >= 4:
             break
a|b|a|b
r1-p003 · Reliance on readlines Returning a List for Indexing Inside Loop
1with open('tmp.txt', 'w') as f:
2 f.write('apple\nbanana\ncherry')
3
4with open('tmp.txt') as fp:
5 trace = []
6 for line in fp.readlines(): # Rule triggers here
7 try:
8 trace.append(f'{len(fp.readlines())}:{line.strip()}')
9 except AttributeError as e:
10 trace.append(f'ERR:{type(e).__name__}')
11 if len(trace) >= 2:
12 break
13print(','.join(trace))
--- orig.py	2026-10-22 13:16:52.516694685 -0400
+++ fixed.py	2026-10-22 13:16:56.477756573 -0400
@@ -3,7 +3,7 @@
 
 with open('tmp.txt') as fp:
     trace = []
-    for line in fp.readlines():  # Rule triggers here
+    for line in fp:  # Rule triggers here
         try:
             trace.append(f'{len(fp.readlines())}:{line.strip()}')
         except AttributeError as e:
0:apple,0:banana
r1-p004 · Boolean Coercion in Loop Context After Fix
1with open('tmp.txt', 'w') as f:
2 f.write('0\n1\n')
3
4with open('tmp.txt') as fp:
5 trace = []
6 for line in fp.readlines(): # Rule triggers here
7 trace.append(str(bool(fp))) # bool(fp) is True for open file
8 if len(trace) >= 2:
9 break
10print(','.join(trace))
--- orig.py	2026-10-22 13:16:52.516694685 -0400
+++ fixed.py	2026-10-22 13:16:56.775761231 -0400
@@ -3,7 +3,7 @@
 
 with open('tmp.txt') as fp:
     trace = []
-    for line in fp.readlines():  # Rule triggers here
+    for line in fp:  # Rule triggers here
         trace.append(str(bool(fp)))  # bool(fp) is True for open file
         if len(trace) >= 2:
             break
True,True
r1-p005 · Binary Mode readlines Returns bytes, Fix Breaks Encoding Assumption (generated program does not parse: Traceback (most recent call last):)
1with open('tmp.bin', 'wb') as f:
2 f b'hello\nworld'
3
4with open('tmp.bin', 'rb') as fp:
5 trace = []
6 for line in fp.readlines(): # Rule triggers here
7 try:
8 trace.append(line.decode().strip())
9 except AttributeError as e:
10 trace.append(f'DECODE_ERR:{type(e).__name__}')
11 if len(trace) >= 2:
12 break
13print('|'.join(trace))
r1-p006 · Type Annotation Evaluated at Runtime Depending on Iteration
1def get_lines():
2 with open('tmp.txt', 'w') as f:
3 f.write('x\ny')
4 with open('tmp.txt') as fp:
5 # This annotation is evaluated at runtime in Python 3.7+
6 lines: list[str] = list(fp.readlines()) # Rule does NOT trigger here (assignment)
7 return [l.upper() for l in lines]
8
9# Separate loop where rule DOES trigger
10with open('tmp.txt') as fp:
11 trace = []
12 for line in fp.readlines(): # Rule triggers here
13 trace.append(line.strip())
14 if len(trace) >= 2:
15 break
16print(','.join(trace))
--- orig.py	2026-10-22 13:16:52.517694701 -0400
+++ fixed.py	2026-10-22 13:16:55.036734053 -0400
@@ -9,7 +9,7 @@
 # Separate loop where rule DOES trigger
 with open('tmp.txt') as fp:
     trace = []
-    for line in fp.readlines():  # Rule triggers here
+    for line in fp:  # Rule triggers here
         trace.append(line.strip())
         if len(trace) >= 2:
             break