ruff FURB103
labelled unknown
Analysis Nemotron 3 Super
Flags patterns where a file is opened using built-in open() and written to via .write() inside a with statement, suggesting replacement with pathlib.Path.write_text() or Path.write_bytes().
Fix: Replaces the with-open-write block with a call to pathlib.Path.write_text() if the mode is text-based (e.g., 'w', 'wt', 'a', 'at') or Path.write_bytes() if the mode is binary (e.g., 'wb', 'ab'), preserving the file path and the argument to write(), assuming the path is a string or Path-compatible object and the write argument matches the expected type.
10 hazards
- The rewrite assumes the file path is a simple string or Path object; if it is an expression with side effects (e.g., a function call), evaluating it once in the Path constructor may change behavior if the original with statement evaluated it multiple times due to __enter__/__exit__ logic.
- If the file path is not a string or bytes (e.g., an integer or custom object), Path() construction may raise TypeError where the original open() might have succeeded via __fspath__ or other protocol support.
- The rewrite does not preserve any comments attached to the with statement or the open() call, which may be lost during autofix.
- If the write() argument is not a literal or simple variable but an expression with side effects (e.g., f.write(get_data())), the autofix still moves it, but if the original code relied on the timing of evaluation within the context manager (e.g., for locking or state), moving it outside could alter behavior.
- The rewrite does not account for custom open overrides via imports (e.g., from mymodule import open), which may not behave like built-in open and thus invalidate the Path substitution.
- If the mode includes flags like '+' (e.g., 'w+', 'r+'), the rule should not fire, but if it does due to incorrect detection, the rewrite would lose read capability.
- The rewrite ignores buffering, errors, and newline parameters; while these often have defaults in write_text/write_bytes, explicit non-default values may change behavior (e.g., custom error handling).
- If the file object is aliased elsewhere in the block (e.g., global f = f) and used after the write, the rewrite breaks because the Path object does not expose the same file handle semantics.
- The autofix may generate invalid syntax if the path contains characters that require escaping in a string literal but are not properly handled (though this is rare and depends on input).
- If the code is part of a larger suite where the file is reopened or reused later, replacing the open with a one-time write may interfere with subsequent operations expecting an open handle.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | invalid | star_imports_module_level_side_effects___all__ | star-imports, module-level side effects, __all__ |
| r1-p002 | invalid | shadowed_builtins_and_names_redefined_later_in_the_module | shadowed builtins and names redefined later in the module |
| r1-p003 | invalid | walrus_comprehensions_scoping_late_binding_in_closures_and_lambdas | walrus, comprehensions scoping, late binding in closures and lambdas |
| r1-p004 | invalid | context_managers_that_are_not_files_objects_with_write_no_close | context managers that are not files, objects with write() but no close() |
| r1-p005 | fired, no fix | rewrite_does_not_preserve_comments_attached_to_with_or_open | The rewrite does not preserve any comments attached to the with statement or the open() call, which may be lost during autofix. |
| r1-p006 | invalid | file_object_aliased_elsewhere_in_block_used_after_write | If the file object is aliased elsewhere in the block (e.g., global f = f) and used after the write, the rewrite breaks because the Path object does not expose the same file handle semantics. |
| r1-p007 | invalid | bytes_vs_str_encoding_and_newline_handling_text_vs_binary_modes | bytes vs str, encoding and newline handling, text vs binary modes |
| r1-p008 | invalid | part_of_larger_suite_file_reopened_or_reused_later | If the code is part of a larger suite where the file is reopened or reused later, replacing the open with a one-time write may interfere with subsequent operations expecting an open handle. |
| r1-p009 | invalid | dict_ordering_duplicate_keys_hash_eq_overrides_sets | dict ordering, duplicate keys, __hash__/__eq__ overrides, sets |
| r1-p010 | invalid | file_modes_truncation_partial_writes_non_str_data_passed_to_write | file modes and truncation, partial writes, non-str data passed to write |
| r2-p001 | silent prediction off | Generators vs Lists Iterators Consumed Once | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r2-p002 | fix ok | Mutable Default Arguments Aliasing After Rewrite | mutable default arguments and aliasing after a rewrite |
| r2-p003 | silent | Custom Open Override Invalidates Path Substitution | The rewrite does not account for custom open overrides via imports (e.g., from mymodule import open) |
| r2-p004 | fix ok | Evaluation Order Side Effects When Expressions Merged | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p005 | silent prediction off | Keyword-Only Positional-Only Args Forwarding Defaults | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p006 | fix ok | Pathlib vs Os.Path Behavior Relative Symlinks Missing | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
| r2-p007 | silent prediction off | Rewrite Ignores Buffering Errors Newline Parameters | The rewrite ignores buffering, errors, and newline parameters; while these often have defaults in write_text/write_bytes, explicit non-default values may change behavior |
| r2-p008 | fired, no fix | Comments Docstrings Attached To Removed Moved Code | comments and docstrings attached to the removed or moved code |
| r2-p009 | fired, no fix | Rewrite Does Not Preserve Comments On With Or Open Call | The rewrite does not preserve any comments attached to the with statement or the open() call, which may be lost during autofix. |
| r2-p010 | silent prediction off | File Object Aliased Elsewhere Used After Write Breaks | If the file object is aliased elsewhere in the block (e.g., global f = f) and used after the write, the rewrite breaks because the Path object does not expose the same file handle semantics. |
Sources
r1-p001 · star_imports_module_level_side_effects___all__ (generated program does not parse: Traceback (most recent call last):)
1__all__ = ['path']
2def _side():
3 global wrote
4 with open(path, 'w') as f:\n # comment before write\n f.write(wrote := 'hello')\npath = 'out.txt'
5wrote = None
6_side()
7with open(path) as f:
8 print(f.read())
9import os
10os.remove(path)
r1-p002 · shadowed_builtins_and_names_redefined_later_in_the_module (generated program does not parse: Traceback (most recent call last):)
1open = lambda *a, **k: None # shadow built-in
2path = 'test.txt'
3with open(path, 'w') as f: # uses shadowed open\n f.write('shadowed') # rule fires but open is not real open\ntry:
4 with open(path) as f:\n print(f.read())\nexcept Exception as e:
5 print(type(e).__name__, e)\nopen = __builtins__.open # restore\nwith open(path, 'w') as f:\n f.write('real')\nwith open(path) as f:\n print(f.read())\nimport os\nos.remove(path)
r1-p003 · walrus_comprehensions_scoping_late_binding_in_closures_and_lambdas (generated program does not parse: Traceback (most recent call last):)
1funs = []
2for i in range(2):
3 path = f'{i}.txt'
4 with open(path, 'w') as f:\n f.write((data := str(i)) * 2)\n funs.append(lambda: open(path).read())\nprint([f() for f in funs])\nfor p in ['0.txt', '1.txt']:
5 import os\nos.remove(p)
r1-p004 · context_managers_that_are_not_files_objects_with_write_no_close (generated program does not parse: Traceback (most recent call last):)
1class FakeFile:
2 def __init__(self, name):
3 self.name = name
4 self.buf = ''
5 def __enter__(self):
6 return self
7 def __exit__(self, *args):
8 pass
9 def write(self, s):
10 self.buf += s
11with FakeFile('dummy') as f:\n f.write('test') # rule silent: not open()\nprint(f.buf)
r1-p005 · rewrite_does_not_preserve_comments_attached_to_with_or_open
1# This comment should be lost
2with open('comments.txt', 'w') as f: # inline comment
3 f.write('keep me') # another
4# after
5with open('comments.txt') as f:
6 print(f.read())
7import os
8os.remove('comments.txt')
keep me
r1-p006 · file_object_aliased_elsewhere_in_block_used_after_write (generated program does not parse: Traceback (most recent call last):)
1global f\nf = None
2with open('alias.txt', 'w') as f:\n f.write('hello')\n global f\n f = f # alias to self\nif f is not None:\n f.write(' world') # second write relies on alias\nwith open('alias.txt') as f:\n print(f.read())\nimport os\nos.remove('alias.txt')
r1-p007 · bytes_vs_str_encoding_and_newline_handling_text_vs_binary_modes (generated program does not parse: Traceback (most recent call last):)
1with open('text.txt', 'w', encoding='utf-8', newline='') as f:
2 f.write('caf\u00e9
3')
4with open('text.txt', 'rb') as f:
5 print(repr(f.read()))
6with open('binary.txt', 'wb') as f:
7 f.write(b'caf\xc3\xa9
8')
9with open('binary.txt', 'rb') as f:
10 print(repr(f.read()))
11import os
12os.remove('text.txt')
13os.remove('binary.txt')
r1-p008 · part_of_larger_suite_file_reopened_or_reused_later (generated program does not parse: Traceback (most recent call last):)
1log = 'log.txt'
2with open(log, 'a') as f:\n f.write('first\n')\n f.write('second\n') # same handle\nwith open(log) as f:\n print(f.read(), end='')\nimport os\nos.remove(log)
r1-p009 · dict_ordering_duplicate_keys_hash_eq_overrides_sets (generated program does not parse: Traceback (most recent call last):)
1class K:
2 def __init__(self, v): self.v = v
3 def __hash__(self): return hash(self.v)
4 def __eq__(self, other): return self.v == other.v
5 def __repr__(self): return f'K({self.v})'
6k1, k2 = K(1), K(1) # same hash, equal
7with open('dict.txt', 'w') as f:\n f.write(str({k1: 'a', k2: 'b'})) # dict may collapse\nwith open('dict.txt') as f:\n print(f.read())\nimport os\nos.remove('dict.txt')
r1-p010 · file_modes_truncation_partial_writes_non_str_data_passed_to_write (generated program does not parse: Traceback (most recent call last):)
1with open('mode.txt', 'w') as f:\n f.write('abcdef')\n f.seek(2)\n f.write('12') # partial overwrite\nwith open('mode.txt') as f:\n print(f.read())\ntry:
2 with open('bad.txt', 'w') as f:\n f.write(123) # type error\nexcept Exception as e:
3 print(type(e).__name__, e)\nimport os\nos.remove('mode.txt')\nif 'bad.txt' in locals(): os.remove('bad.txt')
r2-p001 · Generators vs Lists Iterators Consumed Once
1def gen_lines():
2 yield 'a'
3 yield 'b'
4with open('tmp.txt', 'w') as f:
5 for line in gen_lines():
6 f.write(line + '\\n')
7with open('tmp.txt', 'r') as f:
8 content = f.read()
9print(repr(content))
10import os
11os.remove('tmp.txt')
'a\\nb\\n'
r2-p002 · Mutable Default Arguments Aliasing After Rewrite
1def log(msg, buf=[]):
2 buf.append(msg)
3 return buf
4with open('tmp.txt', 'w') as f:
5 f.write(''.join(log('x')))
6with open('tmp.txt', 'r') as f:
7 print(repr(f.read()))
8import os
9os.remove('tmp.txt')
--- orig.py 2026-10-27 22:06:07.130765550 +0000 +++ fixed.py 2026-10-27 22:06:11.071983658 +0000 @@ -1,8 +1,8 @@ +import pathlib def log(msg, buf=[]): buf.append(msg) return buf -with open('tmp.txt', 'w') as f: - f.write(''.join(log('x'))) +pathlib.Path('tmp.txt').write_text(''.join(log('x'))) with open('tmp.txt', 'r') as f: print(repr(f.read())) import os
'x'
r2-p003 · Custom Open Override Invalidates Path Substitution
1def open(*args, **kwargs):
2 class FakeFile:
3 def __enter__(self): return self
4 def __exit__(self, *exc): pass
5 def write(self, data): self.data = data
6 return FakeFile()
7f = open('tmp.txt', 'w')
8with f as file:
9 file.write('hello')
10print(getattr(file, 'data', 'none'))
hello
r2-p004 · Evaluation Order Side Effects When Expressions Merged
1counter = 0
2def inc():
3 global counter
4 counter += 1
5 return counter
6with open('tmp.txt', 'w') as f:
7 f.write(str(inc()) + '\\n' + str(inc()))
8with open('tmp.txt', 'r') as f:
9 print(repr(f.read()))
10import os
11os.remove('tmp.txt')
--- orig.py 2026-10-27 22:06:07.132765661 +0000 +++ fixed.py 2026-10-27 22:06:10.335942925 +0000 @@ -1,10 +1,10 @@ +import pathlib counter = 0 def inc(): global counter counter += 1 return counter -with open('tmp.txt', 'w') as f: - f.write(str(inc()) + '\\n' + str(inc())) +pathlib.Path('tmp.txt').write_text(str(inc()) + '\\n' + str(inc())) with open('tmp.txt', 'r') as f: print(repr(f.read())) import os
'1\\n2'
r2-p005 · Keyword-Only Positional-Only Args Forwarding Defaults
1def write_file(*, path, data, mode='w', **kw):
2 with open(path, mode, **kw) as f:
3 f.write(data)
4write_file(path='tmp.txt', data='hello', encoding='utf-8')
5with open('tmp.txt', 'r', encoding='utf-8') as f:
6 print(repr(f.read()))
7import os
8os.remove('tmp.txt')
'hello'
r2-p006 · Pathlib vs Os.Path Behavior Relative Symlinks Missing
1import os
2os.symlink('target.txt', 'link.txt')
3with open('link.txt', 'w') as f:
4 f.write('via link')
5with open('target.txt', 'r') as f:
6 print(repr(f.read()))
7os.unlink('link.txt')
8os.remove('target.txt')
--- orig.py 2026-10-27 22:06:07.133765716 +0000 +++ fixed.py 2026-10-27 22:06:10.733964952 +0000 @@ -1,7 +1,7 @@ import os +import pathlib os.symlink('target.txt', 'link.txt') -with open('link.txt', 'w') as f: - f.write('via link') +pathlib.Path('link.txt').write_text('via link') with open('target.txt', 'r') as f: print(repr(f.read())) os.unlink('link.txt')
'via link'
r2-p007 · Rewrite Ignores Buffering Errors Newline Parameters
1with open('tmp.txt', 'w', buffering=1, newline='\\n') as f:
2 f.write('hello')
3with open('tmp.txt', 'rb') as f:
4 print(repr(f.read()))
5import os
6os.remove('tmp.txt')
r2-p008 · Comments Docstrings Attached To Removed Moved Code
1with open('tmp.txt', 'w') as f: # write greeting
2 f.write('hi') # and newline
3with open('tmp.txt', 'r') as f:
4 print(repr(f.read()))
5import os
6os.remove('tmp.txt')
'hi'
r2-p009 · Rewrite Does Not Preserve Comments On With Or Open Call
1# important log
2with open('tmp.txt', 'w') as f: # safety first
3 f.write('ok')
4with open('tmp.txt', 'r') as f:
5 print(repr(f.read()))
6import os
7os.remove('tmp.txt')
'ok'
r2-p010 · File Object Aliased Elsewhere Used After Write Breaks
1f_handle = None
2with open('tmp.txt', 'w') as f:
3 f_handle = f
4 f.write('data')
5if not f_handle.closed:
6 f_handle.write('more')
7with open('tmp.txt', 'r') as f:
8 print(repr(f.read()))
9import os
10os.remove('tmp.txt')
'data'