# ruff FURB129: fix changes program behavior

Internal evidence sheet generated by fixhunt. Not an issue text. A human re-verifies, checks the duplicates
below, and writes the upstream report in their own words following the project's AI policy.

- Tool: ruff ruff 0.17.1
- Rule: FURB129 (tool labels the fix: unknown, this fix reported as safe)
- Oracle: fix-changes-behavior
- Judge (Nemotron 3 Ultra, advisory): real, severity 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.
- Program: minimized from 12 to 9 lines

## Reproducer

```python
def main():
    with open('tmp.txt', 'w') as f:
        f.write('a\nb')
    with open('tmp.txt') as fp:
        for line in fp.readlines():
            if isinstance(fp.readlines(), list):
                print('is list')
if __name__ == '__main__':
    main()
```

## Commands

```sh
python3 prog.py
ruff check --preview --isolated --select FURB129 --fix prog.py
python3 prog.py
```

## Observed

```
stdout before:
is list
is list
stdout after:
is list
```

## Diff applied by the fix

```diff
--- orig.py	2026-10-22 13:43:10.207160820 -0400
+++ fixed.py	2026-10-22 13:43:12.116787061 -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')
 if __name__ == '__main__':
```

## Why the rewrite is not equivalent (model draft, verify before use)

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.

## Upstream search

Queries: "FURB129 fix", "FURB129 file iteration readlines", "Ruff rule FURB129 iterator optimization", "Replace fp.readlines() with direct file iteration"

- related: https://github.com/astral-sh/ruff/issues/18843
  same FURB129 rule but different defect about comment deletion
- related: https://github.com/astral-sh/ruff/issues/17683
  same FURB129 rule but different defect about missing whitespace
- related: https://github.com/astral-sh/ruff/issues/18231
  same FURB129 rule but different defect about parenthesized variable

<details><summary>All hits</summary>

- [github, closed] unused-function-argument not silenced by prefixed underscore if variable name is "次" — https://github.com/astral-sh/ruff/issues/28721
- [github, closed] [`refurb`] `FURB129` fix can delete comments — https://github.com/astral-sh/ruff/issues/18843
- [github, closed] FURB129 fix should add spaces between tokens — https://github.com/astral-sh/ruff/issues/17683
- [github, closed] FURB129 fix fails when the variable is parenthesized — https://github.com/astral-sh/ruff/issues/18231
- [github, closed] Pack of fuzzer Autofix/Infinite loop errors [2] — https://github.com/astral-sh/ruff/issues/8402

</details>
