ruff FURB103
labelled unknown
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
| program | oracle | label | judge | status | summary |
|---|---|---|---|---|---|
| r1-p007 | fix-changes-behavior | labelled safe | program-bug · low | duplicate | 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 … |
| r1-p009 | fix-changes-behavior | labelled safe | program-bug · low | no match | The 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-p007 | fix-changes-behavior | labelled safe | program-bug · low | duplicate | 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 mes… |
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | FURB103 Hazard 1: File Descriptor Leak / Early Close | Fails to preserve the file descriptor if the original code intended to keep the file open beyond the write operation. |
| r1-p002 | silent prediction off | FURB103 Hazard 2: Modes, Truncation, Partial Writes | File modes and truncation, partial writes, non-str data passed to write |
| r1-p003 | fired, no fix | FURB103 Hazard 3: Comments Discarded | Removes comments attached to the open() call or with statement, as the fix discards the original syntactic form. |
| r1-p004 | silent prediction off | FURB103 Hazard 4: Try/Finally Interference | 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. |
| r1-p005 | fix ok | FURB103 Hazard 5: Evaluation Order and Side Effects | Evaluation order and side effects when expressions are merged, duplicated or deleted |
| r1-p006 | silent prediction off | FURB103 Hazard 6: Buffering Control Lost | Disables buffering control; write_text uses default buffering, ignoring any buffering= argument in the original open(). |
| r1-p007 | fix-changes-behavior | FURB103 Hazard 7: Non-String Data Coercion Failure | 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__. |
| r1-p008 | fix ok | FURB103 Hazard 8: Numeric Coercion and Rounding | int/float/bool coercion, Decimal, division and rounding |
| r1-p009 | fix-changes-behavior | FURB103 Hazard 9: Custom Opener Ignored | Ignores custom openers or file descriptors passed via opener=, which Path.write_text does not support. |
| r1-p010 | fired, no fix | FURB103 Hazard 10: Write Return Value Lost | Affects 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-p001 | fired, no fix | Hazard 1: Timing of file closure | Alters timing of file closure relative to other operations in the same scope, potentially affecting file locking or side effects in concurrent contexts. |
| r2-p002 | fix ok | Hazard 2: Keyword-only args / defaults evaluated once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p003 | fired, no fix | Hazard 3: Comments discarded by fix | Removes comments attached to the open() call or with statement, as the fix discards the original syntactic form. |
| r2-p004 | fired, no fix | Hazard 4: Write return value ignored | 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. |
| r2-p005 | silent prediction off | Hazard 5: Custom opener ignored | Ignores custom openers or file descriptors passed via opener=, which Path.write_text does not support. |
| r2-p006 | silent prediction off | Hazard 6: Path reassignment after open | Breaks if the file path is a variable reassigned after open but before write, since the fix evaluates the path early in Path(...) |
| r2-p007 | fix-changes-behavior | Hazard 7: Non-string write with coercion | 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__. |
| r2-p008 | fired, no fix | Hazard 8: Comments/docstrings on removed code | comments and docstrings attached to the removed or moved code |
| r2-p009 | silent prediction off | Hazard 9: Try/finally interference | 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. |
| r2-p010 | fix ok | Hazard 10: Type annotations evaluated at runtime | type 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