ruff FURB103
labelled unknown
Analysis Nemotron 3 Super
Flags manual file writes using 'open' + 'write' inside a 'with' block when they can be replaced with pathlib.Path.write_text/write_bytes.
Fix: Replaces the 'with open(...) as f: f.write(...)' block with 'Path(...).write_text(...)' or 'Path(...).write_bytes(...)' based on mode, adding 'from pathlib import Path' if needed.
10 hazards
- Removes comments attached to the 'as f' alias or within the with block, as the fix discards the original statement structure.
- Changes behavior if the file mode includes '+' (e.g., 'w+') since write_text/write_bytes only open for writing, not reading.
- Alters behavior when the write call is not the last statement in the block, as later statements in the original with block would be dropped.
- Breaks code if the file variable 'f' is used after the write within the block (e.g., for seeking or telling), since the variable is eliminated.
- May fail if the path expression has side effects (e.g., a function call) and is evaluated multiple times due to context manager entry/exit logic, though unlikely.
- Incorrectly handles binary vs text modes if the mode string is dynamically constructed and not explicitly 'b' or absent, leading to wrong method choice.
- Does not preserve custom encoding or errors parameters beyond UTF-8/default, as write_text uses defaults unless specified, risking encoding mismatches.
- Removes the ability to write multiple times via the same handle in the block, as the fix performs a single write operation.
- Changes timing of file closure: the original block exits cleanly via __exit__, while the fix relies on Path's internal handling, which may differ in error cases.
- If the 'open' call is overridden or shadowed (e.g., local variable named 'open'), the fix incorrectly assumes builtin open and misbehaves.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Post-write seeks | 1. Breaks code if the file variable 'f' is used after the write within the block (e.g., for seeking or telling), since the variable is eliminated. |
| r2-p001 | fix ok | Side Effect in Path | May fail if the path expression has side effects and is evaluated multiple times due to context manager entry/exit logic. |
Sources
r1-p001 · Post-write seeks
1from pathlib import Path
2
3# Hazard 1: f is used after write for seeking
4try:
5 with open('h1.txt', 'w') as f:
6 f.write('abc')
7 # Using f after write is the pattern we want to break
8 f.seek(0)
9 pos = f.tell()
10 print(f"OK (old) seek={pos} tell={f.tell()}")
11except Exception as e:
12 print(f"OK (old) Exception: {type(e).__name__}: {e}")
13
14# The FURB103 fix replaces this with:
15Path('h1.txt').write_text('abc')
16# f is gone, so seeking below would fail
17try:
18 # Simulating what the fix does: f is no longer available
19 Path('h1.txt').seek(0)
20except AttributeError as e:
21 print(f"BUG (fix) AttributeError: {e}")
22
23# Verify the file content is unchanged
24with open('h1.txt', 'r') as v:
25 print(f"Content: {v.read()}")
OK (old) Exception: ValueError: I/O operation on closed file. BUG (fix) AttributeError: 'PosixPath' object has no attribute 'seek' Content: abc
r2-p001 · Side Effect in Path
1import os
2# Hazard 1: path expression with side effect (incrementing counter)
3counter = [0]
4def path_with_side_effect():
5 counter[0] += 1
6 return f"/tmp/hazard1_{counter[0]}.txt"
7
8with open(path_with_side_effect(), 'w') as f:
9 f.write('hello')
10
11print(f"path_calls={counter[0]}, expected=2")
12
13# Cleanup
14for i in range(1, counter[0]+1):
15 try:
16 os.remove(f"/tmp/hazard1_{i}.txt")
17 except FileNotFoundError:
18 pass
19
20print("DONE")
--- orig.py 2026-10-17 15:36:39.351450212 -0400 +++ fixed.py 2026-10-17 15:36:39.504449541 -0400 @@ -1,12 +1,12 @@ import os +import pathlib # Hazard 1: path expression with side effect (incrementing counter) counter = [0] def path_with_side_effect(): counter[0] += 1 return f"/tmp/hazard1_{counter[0]}.txt" -with open(path_with_side_effect(), 'w') as f: - f.write('hello') +pathlib.Path(path_with_side_effect()).write_text('hello') print(f"path_calls={counter[0]}, expected=2")
path_calls=1, expected=2 DONE