fixhunt

ruff FURB103: fix changes program behavior

fix-changes-behaviorlabelled safeprogram-bug · lowduplicate
Findings / 2026-10-27T22-10-17-844Z-aav5-ruff-FURB103 / r1-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 BytesLike:
2 def __str__(self): return 'str fall'
3obj = BytesLike()
4with open('coerce.bin', 'wb') as f:
5 f.write(obj) # works via __bytes__
after
1import pathlib
2class BytesLike:
3 def __str__(self): return 'str fall'
4obj = BytesLike()
5pathlib.Path('coerce.bin').write_bytes(obj) # works via __bytes__

Output

stdout beforeexit 1
(no stdout)
TypeError: a bytes-like object is required, not 'BytesLike'
stdout afterexit 1
(no stdout)
TypeError: memoryview: a bytes-like object is required, not 'BytesLike'
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 because the object lacks a `__bytes__` method, causing a TypeError when `f.write(obj)` is called. The fix changes the error message slightly (from `TypeError: a bytes-like object is required` to `TypeError: memoryview: a bytes-like object is required`) but the program was already broken due to a missing `__bytes__` implementation, unrelated to the rewrite.

Explanation Nemotron 3 Super unverified

The original `open` call uses `'wb'` mode, which expects a file object that accepts bytes; writing `__str__`'s string result would raise a clear TypeError about needing a bytes‑like object. The replacement calls `pathlib.Path(...).write_bytes`, which internally creates a memoryview from the argument before checking its type, so the same string from `__str__` triggers a TypeError that mentions memoryview, altering the error message and thus the observable behavior. Since the exception text differs, the transformation is not semantics‑preserving.

Reproduce

file
prog.py (the before program)
minimized program
r1-p007.py
evidence sheet
r1-p007.md
regression fixture
r1-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 binary · ruff rule FURB103 pathlib Path write_bytes · FURB103 coercion to bytes

  • duplicate astral-sh/ruff/issues/26920 Report 26920 describes the same defect: the FURB103 fix replaces open/write with Path.write_bytes, changing behavior because open truncates the file before write fails on a non-bytes-like object.
  • related astral-sh/ruff/issues/26922 Report 26922 covers a similar safety issue for FURB101 (read-whole-file) and mentions FURB103, but the specific construct and error differ.
  • related astral-sh/ruff/issues/20785 Report 20785 states the FURB103 fix breaks code, but the example involves json.dumps and a different failure mode.

Details

program
FURB103 Hazard 7: Non-String Data Coercion Failure
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: a bytes-like object is required, not 'BytesLike' stderr after: TypeError: memoryview: a bytes-like object is required, not 'BytesLike'
run
2026-10-27T22-10-17-844Z-aav5-ruff-FURB103
generated program (8 lines) and the fix's diff
1class BytesLike:
2 def __bytes__(self): return b'converted'
3 def __str__(self): return 'str fall'
4obj = BytesLike()
5with open('coerce.bin', 'wb') as f:
6 f.write(obj) # works via __bytes__
7with open('coerce.bin', 'rb') as f:
8 print(repr(f.read()))
--- orig.py	2026-10-27 22:10:35.167580901 +0000
+++ fixed.py	2026-10-27 22:10:39.271807521 +0000
@@ -1,8 +1,8 @@
+import pathlib
 class BytesLike:
     def __bytes__(self): return b'converted'
     def __str__(self): return 'str fall'
 obj = BytesLike()
-with open('coerce.bin', 'wb') as f:
-    f.write(obj)  # works via __bytes__
+pathlib.Path('coerce.bin').write_bytes(obj)  # works via __bytes__
 with open('coerce.bin', 'rb') as f:
     print(repr(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)