fixhunt

ruff FURB103

labelled unknown
Runs / 2026-10-17T19-36-31-798Z-xrrn-ruff-FURB103ruff 0.17.12026-10-17 19:36 UTC8 programs × 2 round(s)2 programs, 1 fired, 1 fixed, 0 findings, 0 rejectedmodel cost $0.0010

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

idresulttitlehazard
r1-p001silent prediction offPost-write seeks1. 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-p001fix okSide Effect in PathMay 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