ruff FURB103
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Side effect in file path expression | 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 |
| r1-p002 | fix ok | Type annotations evaluated at runtime | type annotations evaluated at runtime, typing-only imports |
| r1-p003 | fix ok | Truthiness of custom __bool__/__len__ | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r1-p004 | silent | Mode 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-p005 | fix ok | Exception suppression in with block | 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. |
| r1-p006 | silent prediction off | Shadowed builtins and redefined names | shadowed builtins and names redefined later in the module |
| r1-p007 | fix ok | Complex write argument and breakpoint safety | 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 |
| r1-p008 | fix ok | Evaluation order and side effects in merged expressions | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p001 | fix ok | String Formatting Edge Cases | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p002 | silent | Mode with Plus Flag | 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. |
| r2-p003 | fix ok | Complex Write Argument and Breakpoint Safety | 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 |
| r2-p004 | fix ok | Walrus, Comprehensions Scoping, Late Binding | walrus, comprehensions scoping, late binding in closures and lambdas |
| r2-p005 | silent prediction off | __del__ or Context Manager __exit__ Reliance | 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 garbag |
| r2-p006 | fix ok | Evaluation Order and Side Effects in Merged Expressions | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p007 | invalid | Conditional Write Affected by Control Flow | 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. |
| r2-p008 | fix ok | Star-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')