ruff FURB129
labelled unknown
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
| program | oracle | label | judge | status | summary |
|---|---|---|---|---|---|
| r1-p003 | fix-changes-behavior | labelled safe | real · high | related | The 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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Mock Object with Custom readlines Property | 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 |
| r1-p002 | fix ok | Walrus Assignment in Loop Condition Affecting Scoping | walrus, comprehensions scoping, late binding in closures and lambdas |
| r1-p003 | fix-changes-behavior | Reliance on readlines Returning a List for Indexing Inside Loop | 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. |
| r1-p004 | fix ok | Boolean Coercion in Loop Context After Fix | int/float/bool coercion, Decimal, division and rounding |
| r1-p005 | invalid | Binary Mode readlines Returns bytes, Fix Breaks Encoding Assumption | bytes vs str, encoding and newline handling, text vs binary modes |
| r1-p006 | fix ok | Type Annotation Evaluated at Runtime Depending on Iteration | type 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