fixhunt

ruff FURB103

labelled unknown
Runs / 2026-10-17T19-38-23-545Z-dfhv-ruff-FURB103ruff 0.17.12026-10-17 19:38 UTC8 programs × 2 round(s)16 programs, 7 fired, 4 fixed, 1 findings, 8 rejectedmodel cost $0.01

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

programoraclelabeljudgestatussummary
r1-p006fix-changes-behaviorlabelled safereal · highduplicateThe 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

idresulttitlehazard
r1-p001fired, no fixSide effect in write argumentIf 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-p002silent prediction offFile object aliasedIf 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-p003fired, no fixMultiple statements, only last is writeIf the with block contains multiple statements and only the last is a write, the fix still removes prior statements, altering behavior.
r1-p004fired, no fixComments and docstrings on removed codecomments and docstrings attached to the removed or moved code
r1-p005fix okComplex file path expressionIf 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-p006fix-changes-behaviorShort-circuiting expression guards writeIf the write call is guarded by a short-circuiting expression (e.g., cond and f.write(s)), the fix hoists it unconditionally.
r1-p007fix okPath variable reassigned after with blockIf the file path variable is reassigned after the with block but before use, the fix may evaluate it too early.
r1-p008fix okMutable default arguments and aliasing after rewritemutable default arguments and aliasing after a rewrite
r2-p001invalidKeyword-only and positional-only parameters with forward defaultskeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p002invalidSide effect in write argument evaluated twice due to hoistIf 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-p003invalidNon-file context manager with write() but no close()context managers that are not files, objects with write() but no close()
r2-p004invalidEvaluation order: write arg uses variable modified in blockevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p005invalidMutable default and aliasing after rewritemutable default arguments and aliasing after a rewrite
r2-p006invalidShort-circuiting expression hoisted unconditionallyIf the write call is guarded by a short-circuiting expression (e.g., cond and f.write(s)), the fix hoists it unconditionally.
r2-p007invalidMode with '+' incorrectly assumed write-onlyIf the mode includes flags like '+' (e.g., 'w+'), the fix incorrectly assumes write-only semantics and drops reading capability.
r2-p008invalidBinary write with partial data and truncation edgefile 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')