ruff FURB103
labelled unknown
Analysis Nemotron 3 Super
Flags patterns where a file is opened, written to once within a with block, then closed—suggesting replacement with pathlib.Path.write_text/write_bytes.
Fix: Replaces the with open(...) as f: f.write(...) block with Path(file_path).write_text(...) or Path(file_path).write_bytes(...) depending on mode, adding 'from pathlib import Path' if needed.
10 hazards
- Removes comments attached to the 'with' statement or 'open' call, as the fix discards syntactic structure.
- If the file path variable is reassigned after the with block but before use, the fix may evaluate it too early.
- If the write argument has side effects (e.g., a function call), moving it outside the with block changes evaluation timing relative to other statements in the block.
- If the file object is used elsewhere via alias (e.g., g = f; g.write(...)), the fix breaks because the alias is no longer valid.
- If the mode includes flags like '+' (e.g., 'w+'), the fix incorrectly assumes write-only semantics and drops reading capability.
- If the encoding is specified in text mode, the fix must preserve it—but if omitted, defaults may differ across Python versions or platforms.
- If the open call uses a custom opener via opener= parameter, the fix ignores it, potentially breaking filesystem virtualization.
- If the with block contains multiple statements and only the last is a write, the fix still removes prior statements, altering behavior.
- If the write call is guarded by a short-circuiting expression (e.g., cond and f.write(s)), the fix hoists it unconditionally.
- If the file path is a complex expression (e.g., os.path.join(a, b)), the fix may duplicate its evaluation if not carefully hoisted.
Findings
| program | oracle | label | judge | status | summary |
|---|---|---|---|---|---|
| r1-p006 | fix-changes-behavior | labelled safe | real · high | duplicate | The rule rewrites a guarded write (`flag and f.write(...)`) inside a `with open` block to an unguarded `Path.write_text(...)`, removing the conditional that pr… |
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Side effect in write argument | If the write argument has side effects (e.g., a function call), moving it outside the with block changes evaluation timing relative to other statements in the block. |
| r1-p002 | silent prediction off | File object aliased | If the file object is used elsewhere via alias (e.g., g = f; g.write(...)), the fix breaks because the alias is no longer valid. |
| r1-p003 | fired, no fix | Multiple statements, only last is write | If the with block contains multiple statements and only the last is a write, the fix still removes prior statements, altering behavior. |
| r1-p004 | fired, no fix | Comments and docstrings on removed code | comments and docstrings attached to the removed or moved code |
| r1-p005 | fix ok | Complex file path expression | If the file path is a complex expression (e.g., os.path.join(a, b)), the fix may duplicate its evaluation if not carefully hoisted. |
| r1-p006 | fix-changes-behavior | Short-circuiting expression guards write | If the write call is guarded by a short-circuiting expression (e.g., cond and f.write(s)), the fix hoists it unconditionally. |
| r1-p007 | fix ok | Path variable reassigned after with block | If the file path variable is reassigned after the with block but before use, the fix may evaluate it too early. |
| r1-p008 | fix ok | Mutable default arguments and aliasing after rewrite | mutable default arguments and aliasing after a rewrite |
| r2-p001 | invalid | Keyword-only and positional-only parameters with forward defaults | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p002 | invalid | Side effect in write argument evaluated twice due to hoist | If the write argument has side effects (e.g., a function call), moving it outside the with block changes evaluation timing relative to other statements in the block. |
| r2-p003 | invalid | Non-file context manager with write() but no close() | context managers that are not files, objects with write() but no close() |
| r2-p004 | invalid | Evaluation order: write arg uses variable modified in block | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p005 | invalid | Mutable default and aliasing after rewrite | mutable default arguments and aliasing after a rewrite |
| r2-p006 | invalid | Short-circuiting expression hoisted unconditionally | If the write call is guarded by a short-circuiting expression (e.g., cond and f.write(s)), the fix hoists it unconditionally. |
| r2-p007 | invalid | Mode with '+' incorrectly assumed write-only | If the mode includes flags like '+' (e.g., 'w+'), the fix incorrectly assumes write-only semantics and drops reading capability. |
| r2-p008 | invalid | Binary write with partial data and truncation edge | file modes and truncation, partial writes, non-str data passed to write |
Sources
r1-p001 · Side effect in write argument
1import os
2side = []
3def get_data():
4 side.append('called')
5 return b'test'
6with open('test.bin', 'wb') as f:
7 print('before write')
8 f.write(get_data())
9print('after:', side)
10os.remove('test.bin')
before write after: ['called']
r1-p002 · File object aliased
1with open('tmp.txt', 'w') as f:
2 g = f
3 g.write('hello')
4with open('tmp.txt', 'r') as f:
5 print(f.read())
6import os; os.remove('tmp.txt')
hello
r1-p003 · Multiple statements, only last is write
1log = []
2with open('x.txt', 'w') as f:
3 log.append('start')
4 f.write('data')
5 log.append('end')
6print(log)
7with open('x.txt', 'r') as f:
8 print(repr(f.read()))
9import os; os.remove('x.txt')
['start', 'end'] 'data'
r1-p004 · Comments and docstrings on removed code
1# init
2with open('c.txt', 'w') as f:
3 # write payload
4 f.write('keep') # this line matters
5# done
6with open('c.txt', 'r') as f:
7 print(f.read())
8import os; os.remove('c.txt')
keep
r1-p005 · Complex file path expression
1import os
2call_count = 0
3def path():
4 global call_count
5 call_count += 1
6 return 'p.txt'
7with open(path(), 'w') as f:
8 f.write(str(call_count))
9with open('p.txt', 'r') as f:
10 print(f.read())
11print('path called:', call_count)
12os.remove('p.txt')
--- orig.py 2026-10-17 15:38:33.187466109 -0400 +++ fixed.py 2026-10-17 15:38:33.345464159 -0400 @@ -1,11 +1,11 @@ import os +import pathlib call_count = 0 def path(): global call_count call_count += 1 return 'p.txt' -with open(path(), 'w') as f: - f.write(str(call_count)) +pathlib.Path(path()).write_text(str(call_count)) with open('p.txt', 'r') as f: print(f.read()) print('path called:', call_count)
1 path called: 1
r1-p006 · Short-circuiting expression guards write
1flag = False
2with open('s.txt', 'w') as f:
3 flag and f.write('should not happen')
4with open('s.txt', 'r') as f:
5 print(repr(f.read()))
6import os; os.remove('s.txt')
--- orig.py 2026-10-17 15:38:33.452462839 -0400 +++ fixed.py 2026-10-17 15:38:33.605460950 -0400 @@ -1,6 +1,6 @@ +import pathlib flag = False -with open('s.txt', 'w') as f: - flag and f.write('should not happen') +pathlib.Path('s.txt').write_text('should not happen') with open('s.txt', 'r') as f: print(repr(f.read())) import os; os.remove('s.txt')
''
r1-p007 · Path variable reassigned after with block
1name = 'first.txt'
2with open(name, 'w') as f:
3 f.write('original')
4name = 'second.txt'
5with open(name, 'r') as f:
6 print(f.read())
7import os; os.remove('first.txt'); os.remove('second.txt')
--- orig.py 2026-10-17 15:38:33.717459568 -0400 +++ fixed.py 2026-10-17 15:38:34.116454636 -0400 @@ -1,6 +1,6 @@ +import pathlib name = 'first.txt' -with open(name, 'w') as f: - f.write('original') +pathlib.Path(name).write_text('original') name = 'second.txt' with open(name, 'r') as f: print(f.read())
r1-p008 · Mutable default arguments and aliasing after rewrite
1def log(msg, buf=[]):
2 buf.append(msg)
3 return buf
4with open('log.txt', 'w') as f:
5 f.write(str(log('first')))
6with open('log.txt', 'r') as f:
7 print(f.read())
8import os; os.remove('log.txt')
--- orig.py 2026-10-17 15:38:34.320452107 -0400 +++ fixed.py 2026-10-17 15:38:34.484450074 -0400 @@ -1,8 +1,8 @@ +import pathlib def log(msg, buf=[]): buf.append(msg) return buf -with open('log.txt', 'w') as f: - f.write(str(log('first'))) +pathlib.Path('log.txt').write_text(str(log('first'))) with open('log.txt', 'r') as f: print(f.read()) import os; os.remove('log.txt')
['first']
r2-p001 · Keyword-only and positional-only parameters with forward defaults (generated program does not parse: Traceback (most recent call last):)
1def writer(data, *, sep='\n', _ignore=print('default')):\n with open('tmp.txt', 'w', encoding='utf-8') as f:\n f.write(str(data) + sep)\nwriter(42)\nwith open('tmp.txt') as f:\n print(repr(f.read()))\nimport os\nos.remove('tmp.txt')
r2-p002 · Side effect in write argument evaluated twice due to hoist (generated program does not parse: Traceback (most recent call last):)
1counter = 0\ndef inc():\n global counter\n counter += 1\n return str(counter)\nwith open('tmp.txt', 'w') as f:\n f.write(inc())\n f.write(inc())\nprint('counter:', counter)\nwith open('tmp.txt') as f:\n print('file:', repr(f.read()))\nimport os\nos.remove('tmp.txt')
r2-p003 · Non-file context manager with write() but no close() (generated program does not parse: Traceback (most recent call last):)
1class SilentWriter:\n def __enter__(self):\n return self\n def __exit__(self, *exc):\n pass\n def write(self, data):\n self.data = data\nwith SilentWriter() as w:\n w.write('hello')\nprint(writer.data if hasattr(writer, 'data') else 'none')\n# Note: writer not defined; meant to show no file touched\nprint('no file created')
r2-p004 · Evaluation order: write arg uses variable modified in block (generated program does not parse: Traceback (most recent call last):)
1x = 0\nwith open('tmp.txt', 'w') as f:\n x = 1\n f.write(str(x))\nprint('x after block:', x)\nwith open('tmp.txt') as f:\n print('file contains:', repr(f.read()))\nimport os\nos.remove('tmp.txt')
r2-p005 · Mutable default and aliasing after rewrite (generated program does not parse: Traceback (most recent call last):)
1log = []\ndef log_writer(msg, buf=log):\n with open('tmp.txt', 'w') as f:\n buf.append(msg)\n f.write(msg)\n return buf\nlog_writer('first')\nlog_writer('second')\nprint('log:', log)\nwith open('tmp.txt') as f:\n print('file:', repr(f.read()))\nimport os\nos.remove('tmp.txt')
r2-p006 · Short-circuiting expression hoisted unconditionally (generated program does not parse: Traceback (most recent call last):)
1enabled = False\nwith open('tmp.txt', 'w') as f:\n enabled and f.write('should not happen')\nprint('enabled:', enabled)\ntry:\n with open('tmp.txt') as f:\n content = f.read()\n print('file:', repr(content))\nexcept FileNotFoundError as e:\n print('FileNotFoundError:', e)\nimport os\nif os.path.exists('tmp.txt'):\n os.remove('tmp.txt')
r2-p007 · Mode with '+' incorrectly assumed write-only (generated program does not parse: Traceback (most recent call last):)
1with open('tmp.txt', 'w+') as f:\n f.write('hello')\n f.seek(0)\n print('read back:', repr(f.read()))\n# After bad fix: Path(...).write_text(...) loses read ability\n# But we test actual behavior\nimport os\nos.remove('tmp.txt')
r2-p008 · Binary write with partial data and truncation edge (generated program does not parse: Traceback (most recent call last):)
1data = b'abcdefghij'\nwith open('tmp.bin', 'wb') as f:\n f.write(data[:5]) # only first 5 bytes\nwith open('tmp.bin', 'rb') as f:\n got = f.read()\n print('wrote:', repr(data[:5]))\n print('got:', repr(got))\n assert got == data[:5]\nimport os\nos.remove('tmp.bin')