fixhunt

ruff FURB103: fix changes program behavior

fix-changes-behaviorlabelled safeprogram-bug · lowduplicate
Findings / 2026-10-27T22-10-17-844Z-aav5-ruff-FURB103 / r2-p007ruff 0.17.18 → 5 lines2026-10-27 22:10 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

Incorrectly handles non-string/write-incompatible data types; write_text expects str, write_bytes expects bytes, but the original .write() might have relied on coercion or custom __write__.

Program

before
1class Num:
2 def __init__(self, v): self.value = v
3n = Num(42)
4with open('num.txt', 'w') as f:
5 f.write(n)
after
1import pathlib
2class Num:
3 def __init__(self, v): self.value = v
4n = Num(42)
5pathlib.Path('num.txt').write_text(n)

Output

stdout beforeexit 1
(no stdout)
TypeError: write() argument must be str, not Num
stdout afterexit 1
(no stdout)
TypeError: data must be str, not Num
fired
1 before · 0 after
runs
each version twice, same file name, no network
minimizer
21 oracle evaluations

Judge Nemotron 3 Ultra advisory program-bug · low

The program already fails before the fix with a TypeError because f.write() requires a string argument but receives a Num object. The fix changes the error message from 'write() argument must be str' to 'data must be str' but both are TypeErrors from the same root cause: passing a non-string to a write operation. The behavioral difference is solely due to the pre-existing failure, not the rewrite itself.

Explanation Nemotron 3 Super unverified

The original code attempts to call `f.write(n)` where `n` is a `Num` instance; Python raises a `TypeError` because `write()` expects a string or bytes object. The rewrite replaces this with `pathlib.Path(...).write_text(n)`, which also requires its argument to be a string (or bytes) but internally performs a similar type check before writing. Both versions fail with a `TypeError` due to passing a non-string `Num` object, so the observable behavior—raising a type error—is unchanged. The fix does not alter semantics; it merely rewrites the call while preserving the same type constraint violation. Thus, the rewrite is semantics‑preserving despite the tool labeling it unsafe.

Reproduce

file
prog.py (the before program)
minimized program
r2-p007.py
evidence sheet
r2-p007.md
regression fixture
r2-p007.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 FURB103 --fix prog.py
python3 prog.py

Upstream search GitHub issues Tavily Nemotron 3 Super + Ultra

FURB103 fix · FURB103 file write text conversion · ruff FURB103 Path write_text usage · FURB103 open write to pathlib migration

  • duplicate astral-sh/ruff/issues/26920 Report 26920 describes the same defect: the FURB103 fix replaces open/write with Path.write_text, changing behavior because open truncates the file before write fails on a non-str argument.
  • related astral-sh/ruff/issues/26922 Report 26922 is about safe fixes causing runtime errors for both FURB101 and FURB103, a broader class of the same rule defect.
  • related astral-sh/ruff/issues/18409 Report 18409 is about the FURB103 fix suggesting Path.write_text in general, not the specific behavior change on type error.
  • related astral-sh/ruff/issues/20785 Report 20785 shows FURB103 fix breaking code with json.dumps bytes, a different construct but same rule and defect class.

Details

program
Hazard 7: Non-string write with coercion
hazard
Incorrectly handles non-string/write-incompatible data types; write_text expects str, write_bytes expects bytes, but the original .write() might have relied on coercion or custom __write__.
oracle
stderr before: TypeError: write() argument must be str, not Num stderr after: TypeError: data must be str, not Num
run
2026-10-27T22-10-17-844Z-aav5-ruff-FURB103
generated program (8 lines) and the fix's diff
1class Num:
2 def __str__(self): return str(self.value)
3 def __init__(self, v): self.value = v
4n = Num(42)
5with open('num.txt', 'w') as f:
6 f.write(n)
7with open('num.txt') as f:
8 print(f.read())
--- orig.py	2026-10-27 22:10:51.230467835 +0000
+++ fixed.py	2026-10-27 22:10:55.883724773 +0000
@@ -1,8 +1,8 @@
+import pathlib
 class Num:
     def __str__(self): return str(self.value)
     def __init__(self, v): self.value = v
 n = Num(42)
-with open('num.txt', 'w') as f:
-    f.write(n)
+pathlib.Path('num.txt').write_text(n)
 with open('num.txt') as f:
     print(f.read())
rule documentation given to the models
# write-whole-file (FURB103)

Derived from the **refurb** linter.

Fix is sometimes available.

This rule is in preview and is not stable. The `--preview` flag is required for use.

## What it does
Checks for uses of `open` and `write` that can be replaced by `pathlib`
methods, like `Path.write_text` and `Path.write_bytes`.

## Why is this bad?
When writing a single string to a file, it's simpler and more concise
to use `pathlib` methods like `Path.write_text` and `Path.write_bytes`
instead of `open` and `write` calls via `with` statements.

## Example
```python
with open("file.txt", "w") as f:
    f.write("some text")
```

Use instead:
```python
from pathlib import Path

Path("file.txt").write_text("some text")
```

## Fix Safety
This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.

## References
- [Python documentation: `Path.write_bytes`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_bytes)
- [Python documentation: `Path.write_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_text)