fixhunt

ruff FURB103

labelled unknown
Runs / 2026-10-27T22-10-17-844Z-aav5-ruff-FURB103ruff 0.17.12026-10-27 22:10 UTC10 programs × 2 round(s)20 programs, 13 fired, 7 fixed, 3 findings, 0 rejectedmodel cost $0.02

Analysis Nemotron 3 Super

Flags manual file writes using open() and .write() that can be replaced with pathlib.Path.write_text() or write_bytes().

Fix: Replaces the open/write pattern with pathlib.Path(file_path).write_text(content) for text modes or write_bytes for binary modes, assuming a single write call and no intervening operations.

12 hazards
  • Removes comments attached to the open() call or with statement, as the fix discards the original syntactic form.
  • Changes behavior if the file is opened in append mode ('a'), since write_text overwrites the file instead of appending.
  • Alters behavior when multiple write() calls occur on the same handle, as the fix only replaces the first write and drops subsequent ones.
  • Breaks code if the file object is used elsewhere after the write (e.g., flushed, seeked), since the fix closes the file immediately after writing.
  • 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__.
  • Ignores custom openers or file descriptors passed via opener=, which Path.write_text does not support.
  • Affecs code relying on the file object's return value from write() (e.g., chars written), as the fix does not capture or return it.
  • Disables buffering control; write_text uses default buffering, ignoring any buffering= argument in the original open().
  • Breaks if the file path is a variable reassigned after open but before write, since the fix evaluates the path early in Path(...)
  • Unsafe if the write() call is inside a try block where finally closes the file, as the fix may close the file earlier or skip the finally logic.
  • Alters timing of file closure relative to other operations in the same scope, potentially affecting file locking or side effects in concurrent contexts.
  • Fails to preserve the file descriptor if the original code intended to keep the file open beyond the write operation.

Findings

programoraclelabeljudgestatussummary
r1-p007fix-changes-behaviorlabelled safeprogram-bug · lowduplicateThe 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 …
r1-p009fix-changes-behaviorlabelled safeprogram-bug · lowno matchThe original program already fails with OSError: [Errno 9] Bad file descriptor because os.open returns an integer file descriptor, but the built-in open() func…
r2-p007fix-changes-behaviorlabelled safeprogram-bug · lowduplicateThe 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 mes…

Programs

idresulttitlehazard
r1-p001silent prediction offFURB103 Hazard 1: File Descriptor Leak / Early CloseFails to preserve the file descriptor if the original code intended to keep the file open beyond the write operation.
r1-p002silent prediction offFURB103 Hazard 2: Modes, Truncation, Partial WritesFile modes and truncation, partial writes, non-str data passed to write
r1-p003fired, no fixFURB103 Hazard 3: Comments DiscardedRemoves comments attached to the open() call or with statement, as the fix discards the original syntactic form.
r1-p004silent prediction offFURB103 Hazard 4: Try/Finally InterferenceUnsafe if the write() call is inside a try block where finally closes the file, as the fix may close the file earlier or skip the finally logic.
r1-p005fix okFURB103 Hazard 5: Evaluation Order and Side EffectsEvaluation order and side effects when expressions are merged, duplicated or deleted
r1-p006silent prediction offFURB103 Hazard 6: Buffering Control LostDisables buffering control; write_text uses default buffering, ignoring any buffering= argument in the original open().
r1-p007fix-changes-behaviorFURB103 Hazard 7: Non-String Data Coercion FailureIncorrectly 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__.
r1-p008fix okFURB103 Hazard 8: Numeric Coercion and Roundingint/float/bool coercion, Decimal, division and rounding
r1-p009fix-changes-behaviorFURB103 Hazard 9: Custom Opener IgnoredIgnores custom openers or file descriptors passed via opener=, which Path.write_text does not support.
r1-p010fired, no fixFURB103 Hazard 10: Write Return Value LostAffects code relying on the file object's return value from write() (e.g., chars written), as the fix does not capture or return it.
r2-p001fired, no fixHazard 1: Timing of file closureAlters timing of file closure relative to other operations in the same scope, potentially affecting file locking or side effects in concurrent contexts.
r2-p002fix okHazard 2: Keyword-only args / defaults evaluated oncekeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p003fired, no fixHazard 3: Comments discarded by fixRemoves comments attached to the open() call or with statement, as the fix discards the original syntactic form.
r2-p004fired, no fixHazard 4: Write return value ignoredAffecs code relying on the file object's return value from write() (e.g., chars written), as the fix does not capture or return it.
r2-p005silent prediction offHazard 5: Custom opener ignoredIgnores custom openers or file descriptors passed via opener=, which Path.write_text does not support.
r2-p006silent prediction offHazard 6: Path reassignment after openBreaks if the file path is a variable reassigned after open but before write, since the fix evaluates the path early in Path(...)
r2-p007fix-changes-behaviorHazard 7: Non-string write with coercionIncorrectly 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__.
r2-p008fired, no fixHazard 8: Comments/docstrings on removed codecomments and docstrings attached to the removed or moved code
r2-p009silent prediction offHazard 9: Try/finally interferenceUnsafe if the write() call is inside a try block where finally closes the file, as the fix may close the file earlier or skip the finally logic.
r2-p010fix okHazard 10: Type annotations evaluated at runtimetype annotations evaluated at runtime, typing-only imports

Sources

r1-p001 · FURB103 Hazard 1: File Descriptor Leak / Early Close
1f = open('temp.txt', 'w'); print(f.fileno()); f.write('data'); print('still open:', not f.closed)
2f.close(); print('closed after explicit close:', f.closed)
3
still open: True
closed after explicit close: True
r1-p002 · FURB103 Hazard 2: Modes, Truncation, Partial Writes
1with open('test.bin', 'wb') as f:
2 f.truncate(5)
3 n = f.write(b'123456')
4 print('wrote', n, 'bytes')
5with open('test.bin', 'rb') as f:
6 print('content:', f.read())
7import os; os.remove('test.bin')
wrote 6 bytes
r1-p003 · FURB103 Hazard 3: Comments Discarded
1# log file
2with open('log.txt', 'w') as f: # write mode
3 f.write('hello') # first line
4print(open('log.txt').read())
hello
r1-p004 · FURB103 Hazard 4: Try/Finally Interference
1f = None
2try:
3 f = open('tmp.txt', 'w')
4 f.write('data')
5finally:
6 if f:
7 f.close()
8 print('finally closed')
9print('file closed after:', f is None or f.closed)
finally closed
file closed after: True
r1-p005 · FURB103 Hazard 5: Evaluation Order and Side Effects
1def get_path():
2 print('path called')
3 return 'out.txt'
4def get_data():
5 print('data called')
6 return 'x'
7with open(get_path(), 'w') as f:
8 f.write(get_data())
9print('done')
--- orig.py	2026-10-27 22:10:32.774448762 +0000
+++ fixed.py	2026-10-27 22:10:36.232639710 +0000
@@ -1,9 +1,9 @@
+import pathlib
 def get_path():
     print('path called')
     return 'out.txt'
 def get_data():
     print('data called')
     return 'x'
-with open(get_path(), 'w') as f:
-    f.write(get_data())
+pathlib.Path(get_path()).write_text(get_data())
 print('done')
path called
data called
done
r1-p006 · FURB103 Hazard 6: Buffering Control Lost
1with open('buf.txt', 'w', buffering=1) as f:
2 f.write('a')
3 f.flush()
4 print('line buffered')
5with open('buf.txt', 'r') as f:
6 print(repr(f.read()))
line buffered
'a'
r1-p007 · FURB103 Hazard 7: Non-String Data Coercion Failure
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()))
r1-p008 · FURB103 Hazard 8: Numeric Coercion and Rounding
1from decimal import Decimal
2val = Decimal('10.5')
3with open('num.txt', 'w') as f:
4 f.write(str(val)) # relies on str()
5with open('num.txt') as f:
6 print('read:', f.read())
--- orig.py	2026-10-27 22:10:35.637606854 +0000
+++ fixed.py	2026-10-27 22:10:38.976791231 +0000
@@ -1,6 +1,6 @@
 from decimal import Decimal
+import pathlib
 val = Decimal('10.5')
-with open('num.txt', 'w') as f:
-    f.write(str(val))  # relies on str()
+pathlib.Path('num.txt').write_text(str(val))  # relies on str()
 with open('num.txt') as f:
     print('read:', f.read())
read: 10.5
r1-p009 · FURB103 Hazard 9: Custom Opener Ignored
1import os
2# Use fd 3 via dup
3fd = os.open('custom.txt', os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
4os.set_inheritable(fd, True)
5with open(fd, 'w') as f:
6 f.write('via fd')
7os.close(fd)
8with open('custom.txt') as f:
9 print(repr(f.read()))
--- orig.py	2026-10-27 22:10:36.039629052 +0000
+++ fixed.py	2026-10-27 22:10:39.566823810 +0000
@@ -1,9 +1,9 @@
 import os
+import pathlib
 # Use fd 3 via dup
 fd = os.open('custom.txt', os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
 os.set_inheritable(fd, True)
-with open(fd, 'w') as f:
-    f.write('via fd')
+pathlib.Path(fd).write_text('via fd')
 os.close(fd)
 with open('custom.txt') as f:
     print(repr(f.read()))
r1-p010 · FURB103 Hazard 10: Write Return Value Lost
1with open('ret.txt', 'w') as f:
2 n = f.write('hello')
3 print('wrote', n, 'chars')
4if n != 5:
5 raise AssertionError('expected 5')
wrote 5 chars
r2-p001 · Hazard 1: Timing of file closure
1import os
2path = 'test.txt'
3with open(path, 'w') as f:
4 f.write('hello')
5 os.remove(path)
6print(os.path.exists(path))
False
r2-p002 · Hazard 2: Keyword-only args / defaults evaluated once
1def writer(data, *, path='out.txt'):
2 with open(path, 'w') as f:
3 f.write(data)
4writer('first')
5writer('second', path='alt.txt')
6with open('out.txt') as f: print(f.read(), end='|')
7with open('alt.txt') as f: print(f.read())
--- orig.py	2026-10-27 22:10:49.012345358 +0000
+++ fixed.py	2026-10-27 22:10:52.933561874 +0000
@@ -1,6 +1,6 @@
+import pathlib
 def writer(data, *, path='out.txt'):
-    with open(path, 'w') as f:
-        f.write(data)
+    pathlib.Path(path).write_text(data)
 writer('first')
 writer('second', path='alt.txt')
 with open('out.txt') as f: print(f.read(), end='|')
first|second
r2-p003 · Hazard 3: Comments discarded by fix
1# This comment should affect tooling
2with open('tmp.txt', 'w') as f: # inline comment
3 f.write('marked')
4# After fix, both comments are lost
5with open('tmp.txt') as f:
6 print(f.read())
marked
r2-p004 · Hazard 4: Write return value ignored
1with open('count.txt', 'w') as f:
2 n = f.write('12345')
3print(n)
5
r2-p005 · Hazard 5: Custom opener ignored
1def my_opener(path, flags):
2 return os.open(path, flags | os.O_CREAT)
3with open('custom.txt', 'w', opener=my_opener) as f:
4 f.write('opener used')
5with open('custom.txt') as f:
6 print(f.read())
r2-p006 · Hazard 6: Path reassignment after open
1p = 'orig.txt'
2f = open(p, 'w')
3p = 'should_not_matter.txt'
4f.write('went to orig')
5f.close()
6with open('orig.txt') as f: print(f.read().strip())
went to orig
r2-p007 · Hazard 7: Non-string write with coercion
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())
r2-p008 · Hazard 8: Comments/docstrings on removed code
1def process():
2 """Process data."""
3 with open('doc.txt', 'w') as f: # write result
4 f.write('done')
5process()
6with open('doc.txt') as f:
7 print(f.read())
done
r2-p009 · Hazard 9: Try/finally interference
1f = None
2try:
3 f = open('finally.txt', 'w')
4 f.write('inside try')
5except Exception as e:
6 print('except:', e)
7finally:
8 if f:
9 f.close()
10 print('finally closed')
11with open('finally.txt') as f:
12 print(f.read())
finally closed
inside try
r2-p010 · Hazard 10: Type annotations evaluated at runtime
1from __future__ import annotations
2path: str = 'anno.txt'
3with open(path, 'w') as f:
4 f.write('typed')
5with open('anno.txt') as f:
6 print(f.read())
--- orig.py	2026-10-27 22:10:52.832556297 +0000
+++ fixed.py	2026-10-27 22:10:56.420754426 +0000
@@ -1,6 +1,6 @@
 from __future__ import annotations
+import pathlib
 path: str = 'anno.txt'
-with open(path, 'w') as f:
-    f.write('typed')
+pathlib.Path(path).write_text('typed')
 with open('anno.txt') as f:
     print(f.read())
typed