fixhunt

ruff FURB103

labelled unknown
Runs / 2026-10-17T19-41-52-280Z-99rj-ruff-FURB103ruff 0.17.12026-10-17 19:41 UTC8 programs × 2 round(s)16 programs, 11 fired, 11 fixed, 0 findings, 1 rejectedmodel cost $0.0040

Analysis Nemotron 3 Super

Flags patterns where a file is opened, written to once, and closed within a with statement using open() and .write(), suggesting replacement with pathlib.Path.write_text() or .write_bytes().

Fix: Replaces the with open(...) as f: f.write(content) block with either Path(file_path).write_text(content, encoding) if mode is 'w' or 'wt' with optional encoding, or Path(file_path).write_bytes(content) if mode is 'wb', preserving the file path expression and write argument.

10 hazards
  • The rewrite drops any comments attached to the with statement or the open() call, as the fix is marked unsafe when comments are present.
  • If the file path expression has side effects (e.g., a function call), the rewrite may evaluate it once instead of potentially multiple times in error handling paths, though in practice it's evaluated once in both forms.
  • When the write argument is a complex expression, moving it into Path.write_text()/write_bytes() does not change evaluation timing since both evaluate the argument before the call, but refactoring may affect debugging breakpoints.
  • If the alias 'f' is used elsewhere in the with block after the write (e.g., for logging), removing the with block breaks that usage.
  • The rewrite assumes the mode is strictly text or binary write; if the mode includes flags like '+' (e.g., 'w+'), the equivalent pathlib method does not exist, making the fix inapplicable or incorrect.
  • If a custom opener is passed via the opener parameter to open(), the pathlib replacement cannot replicate this behavior, leading to different file opening semantics.
  • The rewrite does not preserve the file descriptor's inheritance properties or buffering behavior explicitly set via buffering parameter, though default buffering is similar.
  • If the write call is conditional on a variable that also affects control flow (unlikely due to single-write constraint), the refactor doesn't alter logic but removes the conditional wrapper.
  • In rare cases where __del__ or context manager __exit__ relies on the file object being alive beyond the write (impossible here due to block scope), early termination could occur, but CPython's garbage collection timing makes this speculative.
  • If the code relies on the with statement suppressing exceptions from open() (e.g., FileNotFoundError), the pathlib version propagates the same exceptions, so behavior is preserved.

Programs

idresulttitlehazard
r1-p001fix okSide effect in file path expressionIf the file path expression has side effects (e.g., a function call), the rewrite may evaluate it once instead of potentially multiple times in error handling paths, though in practice it's evaluated
r1-p002fix okType annotations evaluated at runtimetype annotations evaluated at runtime, typing-only imports
r1-p003fix okTruthiness of custom __bool__/__len__truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r1-p004silentMode with '+' flag (w+)The rewrite assumes the mode is strictly text or binary write; if the mode includes flags like '+' (e.g., 'w+'), the equivalent pathlib method does not exist, making the fix inapplicable or incorrect.
r1-p005fix okException suppression in with blockIf the code relies on the with statement suppressing exceptions from open() (e.g., FileNotFoundError), the pathlib version propagates the same exceptions, so behavior is preserved.
r1-p006silent prediction offShadowed builtins and redefined namesshadowed builtins and names redefined later in the module
r1-p007fix okComplex write argument and breakpoint safetyWhen the write argument is a complex expression, moving it into Path.write_text()/write_bytes() does not change evaluation timing since both evaluate the argument before the call, but refactoring may
r1-p008fix okEvaluation order and side effects in merged expressionsevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p001fix okString Formatting Edge Casesstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p002silentMode with Plus FlagThe rewrite assumes the mode is strictly text or binary write; if the mode includes flags like '+' (e.g., 'w+'), the equivalent pathlib method does not exist, making the fix inapplicable or incorrect.
r2-p003fix okComplex Write Argument and Breakpoint SafetyWhen the write argument is a complex expression, moving it into Path.write_text()/write_bytes() does not change evaluation timing since both evaluate the argument before the call, but refactoring may
r2-p004fix okWalrus, Comprehensions Scoping, Late Bindingwalrus, comprehensions scoping, late binding in closures and lambdas
r2-p005silent prediction off__del__ or Context Manager __exit__ RelianceIn rare cases where __del__ or context manager __exit__ relies on the file object being alive beyond the write (impossible here due to block scope), early termination could occur, but CPython's garbag
r2-p006fix okEvaluation Order and Side Effects in Merged Expressionsevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p007invalidConditional Write Affected by Control FlowIf the write call is conditional on a variable that also affects control flow (unlikely due to single-write constraint), the refactor doesn't alter logic but removes the conditional wrapper.
r2-p008fix okStar-Imports, Module-Level Side Effects, __all__star-imports, module-level side effects, __all__

Sources

r1-p001 · Side effect in file path expression
1import os
2counter = 0
3def get_path():
4 global counter
5 counter += 1
6 return 'tmp.txt'
7try:
8 with open(get_path(), 'w') as f:
9 f.write(str(counter))
10except Exception as e:
11 print(f'Error: {type(e).__name__}: {e}')
12finally:
13 if os.path.exists('tmp.txt'):
14 with open('tmp.txt') as f:
15 print(f'Read: {f.read()}')
16 os.remove('tmp.txt')
17print(f'Counter: {counter}')
--- orig.py	2026-10-17 15:42:01.805959372 -0400
+++ fixed.py	2026-10-17 15:42:01.980955834 -0400
@@ -1,12 +1,12 @@
 import os
+import pathlib
 counter = 0
 def get_path():
     global counter
     counter += 1
     return 'tmp.txt'
 try:
-    with open(get_path(), 'w') as f:
-        f.write(str(counter))
+    pathlib.Path(get_path()).write_text(str(counter))
 except Exception as e:
     print(f'Error: {type(e).__name__}: {e}')
 finally:
Read: 1
Counter: 1
r1-p002 · Type annotations evaluated at runtime
1from typing import List
2T = int
3def get_data() -> T:
4 return 42
5with open('out.txt', 'w') as f:
6 f.write(str(get_data()))
7with open('out.txt') as f:
8 print(f.read())
9import os
10os.remove('out.txt')
--- orig.py	2026-10-17 15:42:02.092953567 -0400
+++ fixed.py	2026-10-17 15:42:02.260950165 -0400
@@ -1,9 +1,9 @@
 from typing import List
+import pathlib
 T = int
 def get_data() -> T:
     return 42
-with open('out.txt', 'w') as f:
-    f.write(str(get_data()))
+pathlib.Path('out.txt').write_text(str(get_data()))
 with open('out.txt') as f:
     print(f.read())
 import os
42
r1-p003 · Truthiness of custom __bool__/__len__
1class BoolObj:
2 def __init__(self, val):
3 self.val = val
4 def __bool__(self):
5 print(f'__bool__ called with {self.val}')
6 return bool(self.val)
7obj = BoolObj(0)
8with open('test.txt', 'w') as f:
9 f.write('yes' if obj else 'no')
10with open('test.txt') as f:
11 print(f.read())
12import os
13os.remove('test.txt')
--- orig.py	2026-10-17 15:42:02.376947817 -0400
+++ fixed.py	2026-10-17 15:42:02.536944578 -0400
@@ -1,3 +1,4 @@
+import pathlib
 class BoolObj:
     def __init__(self, val):
         self.val = val
@@ -5,8 +6,7 @@
         print(f'__bool__ called with {self.val}')
         return bool(self.val)
 obj = BoolObj(0)
-with open('test.txt', 'w') as f:
-    f.write('yes' if obj else 'no')
+pathlib.Path('test.txt').write_text('yes' if obj else 'no')
 with open('test.txt') as f:
     print(f.read())
 import os
__bool__ called with 0
no
r1-p004 · Mode with '+' flag (w+)
1with open('test.txt', 'w+') as f:
2 f.write('hello')
3 f.seek(0)
4 content = f.read()
5 print(f'Read back: {content}')
6import nos
7os.remove('test.txt')
Read back: hello
r1-p005 · Exception suppression in with block
1import os
2if os.path.exists('missing.txt'):
3 os.remove('missing.txt')
4try:
5 with open('missing.txt', 'w') as f:
6 f.write('should not happen')
7except FileNotFoundError as e:
8 print(f'Caught: {e}')
9print('Done')
--- orig.py	2026-10-17 15:42:02.982935548 -0400
+++ fixed.py	2026-10-17 15:42:03.139932366 -0400
@@ -1,9 +1,9 @@
 import os
+import pathlib
 if os.path.exists('missing.txt'):
     os.remove('missing.txt')
 try:
-    with open('missing.txt', 'w') as f:
-        f.write('should not happen')
+    pathlib.Path('missing.txt').write_text('should not happen')
 except FileNotFoundError as e:
     print(f'Caught: {e}')
 print('Done')
Done
r1-p006 · Shadowed builtins and redefined names
1open = lambda *a, **k: __builtins__.open(*a, **k)
2path = 'shadow.txt'
3with open(path, 'w') as f:
4 f.write('test')
5with open(path) as f:
6 print(f.read())
7import os
8os.remove(path)
test
r1-p007 · Complex write argument and breakpoint safety
1def side_effect():
2 print('Evaluating write arg')
3 return 'data'
4with open('tmp.txt', 'w') as f:
5 f.write(side_effect())
6with open('tmp.txt') as f:
7 print(f.read())
8import os
9os.remove('tmp.txt')
--- orig.py	2026-10-17 15:42:03.378927521 -0400
+++ fixed.py	2026-10-17 15:42:03.533924379 -0400
@@ -1,8 +1,8 @@
+import pathlib
 def side_effect():
     print('Evaluating write arg')
     return 'data'
-with open('tmp.txt', 'w') as f:
-    f.write(side_effect())
+pathlib.Path('tmp.txt').write_text(side_effect())
 with open('tmp.txt') as f:
     print(f.read())
 import os
Evaluating write arg
data
r1-p008 · Evaluation order and side effects in merged expressions
1vals = [1, 2, 3]
2idx = 0
3def next_val():
4 global idx
5 if idx >= len(vals):
6 return ''
7 v = vals[idx]
8 idx += 1
9 return str(v)
10with open('seq.txt', 'w') as f:
11 f.write(next_val() + next_val())
12with open('seq.txt') as f:
13 print(f.read())
14import os
15os.remove('seq.txt')
--- orig.py	2026-10-17 15:42:03.644922129 -0400
+++ fixed.py	2026-10-17 15:42:03.794919089 -0400
@@ -1,3 +1,4 @@
+import pathlib
 vals = [1, 2, 3]
 idx = 0
 def next_val():
@@ -7,8 +8,7 @@
     v = vals[idx]
     idx += 1
     return str(v)
-with open('seq.txt', 'w') as f:
-    f.write(next_val() + next_val())
+pathlib.Path('seq.txt').write_text(next_val() + next_val())
 with open('seq.txt') as f:
     print(f.read())
 import os
12
r2-p001 · String Formatting Edge Cases
1def main():
2 name = 'test'
3 fmt = '{}.txt'.format
4 val = 42
5 # Multiple formatting styles used in path and content
6 with open(fmt('out'), 'w') as f:
7 f.write('%s %r' % (val, name))
8 with open(f'{name}_2.txt', 'w') as f:
9 f.write(str(val))
10 # Verify outputs
11 import os
12 print(open('test.out').read().strip())
13 print(open('test_2.txt').read().strip())
14 try:
15 os.remove('test.out')
16 os.remove('test_2.txt')
17 except FileNotFoundError:
18 pass
19
20if __name__ == '__main__':
21 main()
--- orig.py	2026-10-17 15:42:09.712798664 -0400
+++ fixed.py	2026-10-17 15:42:10.094790863 -0400
@@ -1,12 +1,11 @@
+import pathlib
 def main():
     name = 'test'
     fmt = '{}.txt'.format
     val = 42
     # Multiple formatting styles used in path and content
-    with open(fmt('out'), 'w') as f:
-        f.write('%s %r' % (val, name))
-    with open(f'{name}_2.txt', 'w') as f:
-        f.write(str(val))
+    pathlib.Path(fmt('out')).write_text('%s %r' % (val, name))
+    pathlib.Path(f'{name}_2.txt').write_text(str(val))
     # Verify outputs
     import os
     print(open('test.out').read().strip())
r2-p002 · Mode with Plus Flag
1def main():
2 data = b'hello'
3 with open('tmp.bin', 'w+b') as f:
4 f.write(data)
5 f.seek(0)
6 result = f.read()
7 print(result == data)
8 import os
9 os.remove('tmp.bin')
10
11if __name__ == '__main__':
12 main()
True
r2-p003 · Complex Write Argument and Breakpoint Safety
1def get_data():
2 print('get_data called')
3 return 'content'
4
5def main():
6 with open('test.txt', 'w') as f:
7 f.write(get_data())
8 print(open('test.txt').read())
9 import os
10 os.remove('test.txt')
11
12if __name__ == '__main__':
13 main()
--- orig.py	2026-10-17 15:42:10.439783812 -0400
+++ fixed.py	2026-10-17 15:42:10.595780624 -0400
@@ -1,10 +1,10 @@
+import pathlib
 def get_data():
     print('get_data called')
     return 'content'
 
 def main():
-    with open('test.txt', 'w') as f:
-        f.write(get_data())
+    pathlib.Path('test.txt').write_text(get_data())
     print(open('test.txt').read())
     import os
     os.remove('test.txt')
get_data called
content
r2-p004 · Walrus, Comprehensions Scoping, Late Binding
1def main():
2 data = [f'item{i}' for i in range(2)]
3 # Walrus in path expression
4 with open((path := f'file_{len(data)}.txt'), 'w') as f:
5 f.write('\n'.join(data))
6 # Closure capturing path
7 def reader():
8 return open(path).read()
9 print(reader())
10 import os
11 os.remove(path)
12
13if __name__ == '__main__':
14 main()
--- orig.py	2026-10-17 15:42:10.704778396 -0400
+++ fixed.py	2026-10-17 15:42:10.860775208 -0400
@@ -1,8 +1,8 @@
+import pathlib
 def main():
     data = [f'item{i}' for i in range(2)]
     # Walrus in path expression
-    with open((path := f'file_{len(data)}.txt'), 'w') as f:
-        f.write('\n'.join(data))
+    pathlib.Path(path := f'file_{len(data)}.txt').write_text('\n'.join(data))
     # Closure capturing path
     def reader():
         return open(path).read()
item0
item1
r2-p005 · __del__ or Context Manager __exit__ Reliance
1class TrackFile:
2 def __init__(self, f):
3 self.f = f
4 self.alive = True
5 def __del__(self):
6 self.alive = False
7
8def main():
9 tracker = None
10 with open('tmp.txt', 'w') as f:
11 tracker = TrackFile(f)
12 f.write('test')
13 # After with block, f should be closed but tracker may still reference it
14 print('tracker.alive after with:', tracker.alive if tracker else None)
15 print(open('tmp.txt').read())
16 import os
17 os.remove('tmp.txt')
18
19if __name__ == '__main__':
20 main()
tracker.alive after with: True
test
r2-p006 · Evaluation Order and Side Effects in Merged Expressions
1counter = 0
2
3def side_effect():
4 global counter
5 counter += 1
6 return str(counter)
7
8def main():
9 global counter
10 counter = 0
11 # Path expression with side effect
12 with open(side_effect() + '.txt', 'w') as f:
13 f.write(side_effect())
14 print('counter:', counter)
15 print(open('12.txt').read())
16 import os
17 os.remove('12.txt')
18
19if __name__ == '__main__':
20 main()
--- orig.py	2026-10-17 15:42:11.097770361 -0400
+++ fixed.py	2026-10-17 15:42:11.470762729 -0400
@@ -1,3 +1,4 @@
+import pathlib
 counter = 0
 
 def side_effect():
@@ -9,8 +10,7 @@
     global counter
     counter = 0
     # Path expression with side effect
-    with open(side_effect() + '.txt', 'w') as f:
-        f.write(side_effect())
+    pathlib.Path(side_effect() + '.txt').write_text(side_effect())
     print('counter:', counter)
     print(open('12.txt').read())
     import os
counter: 2
r2-p007 · Conditional Write Affected by Control Flow (generated program does not parse: Traceback (most recent call last):)
1def main():
2 flag = True
3 msg = 'hello'
4 with out := open('cond.txt', 'w') if flag else None:
5 if out is not None:
6 out.write(msg)
7 print(open('cond.txt').read())
8 import os
9 os.remove('cond.txt')
10
11if __name__ == '__main__':
12 main()
r2-p008 · Star-Imports, Module-Level Side Effects, __all__
1__all__ = ['helper']
2
3def helper():
4 return 'module_data'
5
6def main():
7 from module import * # type: ignore
8 with open('mod_out.txt', 'w') as f:
9 f.write(helper())
10 print(open('mod_out.txt').read())
11 import os
12 os.remove('mod_out.txt')
13
14if __name__ == '__main__':
15 main()
--- orig.py	2026-10-17 15:42:11.830755363 -0400
+++ fixed.py	2026-10-17 15:42:12.208747623 -0400
@@ -1,3 +1,4 @@
+import pathlib
 __all__ = ['helper']
 
 def helper():
@@ -5,8 +6,7 @@
 
 def main():
     from module import *  # type: ignore
-    with open('mod_out.txt', 'w') as f:
-        f.write(helper())
+    pathlib.Path('mod_out.txt').write_text(helper())
     print(open('mod_out.txt').read())
     import os
     os.remove('mod_out.txt')