fixhunt

ruff FURB129: fix changes program behavior

fix-changes-behaviorlabelled safereal · highrelated
Findings / 2026-10-17T21-30-17-728Z-bxl4-ruff-FURB129 / r2-p008ruff 0.17.112 → 9 lines2026-10-17 21:30 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

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

Program

before
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')
8if __name__ == '__main__':
9 main()
after
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:
6 if isinstance(fp.readlines(), list):
7 print('is list')
8if __name__ == '__main__':
9 main()

Output

stdout beforeexit 0
is list
is list
stdout afterexit 0
is list
fired
1 before · 0 after
runs
each version twice, same file name, no network

Judge Nemotron 3 Ultra advisory real · high

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 then returns an empty list because the file position is at EOF, changing the `isinstance` check from True to False and halving the output. The documentation presents the rewrite as equivalent for iteration but does not account for code that also calls `readlines()` on the same file object inside the loop.

Explanation Nemotron 3 Super unverified

The original loop iterates over `fp.readlines()`, which returns a new list of all lines each time it’s called; inside the loop body, another call to `fp.readlines()` reads from the file again, but since iteration has already consumed the iterator via the first readlines, subsequent calls return empty lists, yet still produce output because the condition checks type only—however, the outer loop runs once per line due to the initial list length. After rewriting to iterate directly over `fp`, the file iterator is advanced incrementally; each iteration consumes one line, leaving fewer unread lines, so later calls to `fp.readlines()` return shorter or empty lists, reducing the number of times the inner condition yields true and thus decreasing output. The change alters when and how file position advances, affecting repeated reads within the loop body.

Reproduce

file
prog.py (the before program)
minimized program
r2-p008.py
evidence sheet
r2-p008.md
regression fixture
r2-p008.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 file iteration readlines · Ruff rule FURB129 iterator optimization · Replace fp.readlines() with direct file iteration

Details

program
List-specific Behavior Reliance
hazard
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
oracle
stdout before: is list is list stdout after: is list
run
2026-10-17T21-30-17-728Z-bxl4-ruff-FURB129
generated program (12 lines) and the fix's diff
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())
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)