fixhunt

ruff FURB129: fix changes program behavior

fix-changes-behaviorlabelled safereal · highrelated
Findings / 2026-10-22T17-16-41-266Z-868l-ruff-FURB129 / r1-p003ruff 0.17.113 → 10 lines2026-10-22 17:16 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

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.

Program

before
1with open('tmp.txt', 'w') as f:
2 f.write('apple\nbanana\ncherry')
3with open('tmp.txt') as fp:
4 trace = []
5 for line in fp.readlines(): # Rule triggers here
6 try:
7 trace.append(f'{len(fp.readlines())}:{line.strip()}')
8 except AttributeError as e:
9 trace.append(f'ERR:{type(e).__name__}')
10print(','.join(trace))
after
1with open('tmp.txt', 'w') as f:
2 f.write('apple\nbanana\ncherry')
3with open('tmp.txt') as fp:
4 trace = []
5 for line in fp: # Rule triggers here
6 try:
7 trace.append(f'{len(fp.readlines())}:{line.strip()}')
8 except AttributeError as e:
9 trace.append(f'ERR:{type(e).__name__}')
10print(','.join(trace))

Output

stdout beforeexit 0
0:apple,0:banana,0:cherry
stdout afterexit 0
2:apple
fired
1 before · 0 after
runs
each version twice, same file name, no network

Judge Nemotron 3 Ultra advisory real · high

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 the loop reads from the current position (which is at the start of the next line after the `for` loop's implicit `readline()`), returning the remaining lines. After the fix, the `for` loop consumes lines one by one via iteration, so the internal `fp.readlines()` sees fewer remaining lines each iteration, producing different output. The documentation presents the rewrite as equivalent for simple iteration but does not account for nested reads on the same file object.

Explanation Nemotron 3 Super unverified

The original loop iterates over lines from `fp.readlines()`, which returns all remaining lines each time it’s called; inside the loop, `fp.readlines()` consumes the rest of the file on the first iteration, returning two more lines (so length 2), then exhausts the file, making subsequent calls return empty lists (length 0). After fixing to iterate directly over `fp`, the loop yields one line per iteration, but `fp.readlines()` now reads from the current file position onward: on the first iteration it reads the remaining two lines (length 2), advancing the file pointer to end‑of‑file, so later iterations see zero remaining lines. Hence only the first iteration produces output (`2:apple`), changing behavior.

Reproduce

file
prog.py (the before program)
minimized program
r1-p003.py
evidence sheet
r1-p003.md
regression fixture
r1-p003.py the minimized program with a header stating the observed change, for the tool's test suite
python3 prog.py
ruff check --preview --isolated --select FURB129 --fix prog.py
python3 prog.py

Upstream search GitHub issues Tavily Nemotron 3 Super + Ultra

FURB129 fix · FURB129 readlines iteration variable reuse · FURB129 file iterator exhaustion bug · FURB129 fp.readlines() inside loop

  • related astral-sh/ruff/issues/18843 Same FURB129 rule but different defect: comment deletion during fix rather than behavior change from readlines() consumption
  • related astral-sh/ruff/issues/17683 Same FURB129 rule but different defect: missing whitespace around tokens causing syntax error rather than semantic behavior change
  • related astral-sh/ruff/issues/18231 Same FURB129 rule but different defect: parenthesized variable causing syntax error rather than readlines() exhaustion changing loop semantics

Details

program
Reliance on readlines Returning a List for Indexing Inside Loop
hazard
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.
oracle
stdout before: 0:apple,0:banana,0:cherry stdout after: 2:apple
run
2026-10-22T17-16-41-266Z-868l-ruff-FURB129
generated program (13 lines) and the fix's diff
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:
rule documentation given to the models
# readlines-in-for (FURB129)

Derived from the **refurb** linter.

Fix is always available.

## What it does
Checks for uses of `readlines()` when iterating over a file line-by-line.

## Why is this bad?
Rather than iterating over all lines in a file by calling `readlines()`,
it's more convenient and performant to iterate over the file object
directly.

## Example
```python
with open("file.txt") as fp:
    for line in fp.readlines():
        ...
```

Use instead:
```python
with open("file.txt") as fp:
    for line in fp:
        ...
```

## Fix safety
This rule's fix is marked as unsafe if there's comments in the
`readlines()` call, as comments may be removed.

For example, the fix would be marked as unsafe in the following case:
```python
with open("file.txt") as fp:
    for line in (  # comment
        fp.readlines()  # comment
    ):
        ...
```

## References
- [Python documentation: `io.IOBase.readlines`](https://docs.python.org/3/library/io.html#io.IOBase.readlines)