fixhunt

ruff SIM208

labelled unknown
Runs / 2026-10-17T20-34-24-321Z-f1do-ruff-SIM208ruff 0.17.12026-10-17 20:34 UTC8 programs × 2 round(s)16 programs, 15 fired, 15 fixed, 0 findings, 1 rejectedmodel cost $0.02

Analysis Nemotron 3 Super

Flags expressions containing two consecutive 'not' operators applied to the same subexpression.

Fix: Removes the outer 'not' and its surrounding parentheses if present, leaving only the inner expression after the inner 'not'.

10 hazards
  • Rewriting 'not (not x)' when 'x' is a property with a side-effecting __get__ method changes behavior because the property is accessed twice in the original but once in the fix.
  • If 'x' is a function call, the original evaluates it twice while the fix evaluates it once, altering side effects and performance.
  • When 'x' is an indexing operation like 'a[b]', the original may raise IndexError twice (if the first access succeeds but the second fails on a mutated object), whereas the fix only risks it once.
  • In 'not (not obj.method())', if 'method' returns a mutable object that changes state on first call, the second call operates on a different state than the fix assumes.
  • For 'not (not x)' where 'x' is a conditional expression like 'a if b else c', the original evaluates the condition twice, potentially observing different values if 'b' is mutable between evaluations.
  • If the inner 'not' applies to a comma-separated tuple like 'not (not (a, b))', removing the outer 'not' and parentheses alters the expression's structure and may break tuple display or unpacking expectations.
  • When the double negation appears inside a larger expression like 'f(not (not x))', the fix changes the number of times 'x' is evaluated, affecting side effects visible to 'f'.
  • If the code relies on the traceback depth for debugging or testing, removing the extra 'not' frame alters the call stack.
  • A comment placed between the 'not' tokens like 'not # comment (not x)' would be lost if the autofix removes the inner 'not' and whitespace without preserving comment placement.
  • In code using 'not (not x)' to deliberately force a boolean context (e.g., for truthiness testing in a numeric setting), the fix may change the type of the result in edge cases involving numpy arrays or custom __bool__ implementations.

Programs

idresulttitlehazard
r1-p001fix okContext manager without close(): resource leak riskcontext managers that are not files, objects with write() but no close()
r1-p002fix okMutable default argument aliasing after rewritemutable default arguments and aliasing after a rewrite
r1-p003fix okFunction call evaluated twice vs once: side effect divergenceIf 'x' is a function call, the original evaluates it twice while the fix evaluates it once, altering side effects and performance.
r1-p004fix okEvaluation order and side effects in merged expressionsevaluation order and side effects when expressions are merged, duplicated or deleted
r1-p005fix okDouble negation inside function call alters evaluation countWhen the double negation appears inside a larger expression like 'f(not (not x))', the fix changes the number of times 'x' is evaluated, affecting side effects visible to 'f'.
r1-p006fix ok prediction offBoolean forcing with numpy-like truthiness altered by fixIn code using 'not (not x)' to deliberately force a boolean context (e.g., for truthiness testing in a numeric setting), the fix may change the type of the result in edge cases involving numpy arrays
r1-p007fix okClass vs instance attribute with property side effectclass attributes vs instance attributes, descriptors, properties with side effects
r1-p008fix okStar-imports and module-level side effectsstar-imports, module-level side effects, __all__
r2-p001fix okDouble Negation Inside Function Call Alters Evaluation CountWhen the double negation appears inside a larger expression like 'f(not (not x))', the fix changes the number of times 'x' is evaluated, affecting side effects visible to 'f'.
r2-p002fix okIndexing Operation May Raise IndexError Twice vs OnceWhen 'x' is an indexing operation like 'a[b]', the original may raise IndexError twice (if the first access succeeds but the second fails on a mutated object), whereas the fix only risks it once.
r2-p003invalidConditional Expression Evaluates Condition TwiceFor 'not (not x)' where 'x' is a conditional expression like 'a if b else c', the original evaluates the condition twice, potentially observing different values if 'b' is mutable between evaluations.
r2-p004fix ok prediction offComment Between Not Tokens Would Be LostA comment placed between the 'not' tokens like 'not # comment\n(not x)' would be lost if the autofix removes the inner 'not' and whitespace without preserving comment placement.
r2-p005fix okPathlib Vs Os.Path Behavior On Relative Paths Etcpathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths
r2-p006fix okMethod Returns Mutable Object That Changes StateIn 'not (not obj.method())', if 'method' returns a mutable object that changes state on first call, the second call operates on a different state than the fix assumes.
r2-p007fix okProperty With Side-Effecting __Get__ Accessed TwiceRewriting 'not (not x)' when 'x' is a property with a side-effecting __get__ method changes behavior because the property is accessed twice in the original but once in the fix.
r2-p008fix okKeyword-Only Positional-Only *Args/**Kwargs Forwarding Defaults Evaluated Oncekeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once

Sources

r1-p001 · Context manager without close(): resource leak risk
1class Writer:
2 def __init__(self, name):
3 self.name = name
4 self.log = []
5 def write(self, data):
6 self.log.append(data)
7 print(f"write: {data}")
8
9w = Writer('test')
10def use_writer():
11 return w
12
13if not (not use_writer()):
14 use_writer().write('hello')
15print('log:', Writer('test').log)
--- orig.py	2026-10-17 16:34:33.988907100 -0400
+++ fixed.py	2026-10-17 16:34:34.164907420 -0400
@@ -10,6 +10,6 @@
 def use_writer():
     return w
 
-if not (not use_writer()):
+if use_writer():
     use_writer().write('hello')
 print('log:', Writer('test').log)
write: hello
log: []
r1-p002 · Mutable default argument aliasing after rewrite
1def f(x=[]):
2 x.append(1)
3 return x
4
5def g():
6 return f()
7
8if not (not g()):
9 print('first:', g())
10else:
11 print('unreachable')
12print('second:', g())
--- orig.py	2026-10-17 16:34:34.267907608 -0400
+++ fixed.py	2026-10-17 16:34:34.439907921 -0400
@@ -5,7 +5,7 @@
 def g():
     return f()
 
-if not (not g()):
+if g():
     print('first:', g())
 else:
     print('unreachable')
first: [1, 1]
second: [1, 1, 1]
r1-p003 · Function call evaluated twice vs once: side effect divergence
1counter = 0
2
3def inc():
4 global counter
5 counter += 1
6 print(f'inc called, counter={counter}')
7 return counter > 0
8
9if not (not inc()):
10 print('branch taken')
11print('final counter:', counter)
--- orig.py	2026-10-17 16:34:34.540908105 -0400
+++ fixed.py	2026-10-17 16:34:34.703908401 -0400
@@ -6,6 +6,6 @@
     print(f'inc called, counter={counter}')
     return counter > 0
 
-if not (not inc()):
+if inc():
     print('branch taken')
 print('final counter:', counter)
inc called, counter=1
branch taken
final counter: 1
r1-p004 · Evaluation order and side effects in merged expressions
1side = []
2
3def log(msg):
4 side.append(msg)
5 return True
6
7result = [log('A'), log('B')]
8if not (not (result[0] and result[1])):
9 side.append('taken')
10print('side effects:', side)
--- orig.py	2026-10-17 16:34:34.808908593 -0400
+++ fixed.py	2026-10-17 16:34:34.971908889 -0400
@@ -5,6 +5,6 @@
     return True
 
 result = [log('A'), log('B')]
-if not (not (result[0] and result[1])):
+if result[0] and result[1]:
     side.append('taken')
 print('side effects:', side)
side effects: ['A', 'B', 'taken']
r1-p005 · Double negation inside function call alters evaluation count
1calls = []
2
3def get_val():
4 calls.append(len(calls))
5 return len(calls) % 2 == 0
6
7def f(arg):
8 return arg, len(calls)
9
10out = f(not (not get_val()))
11print('result:', out)
12calls.clear()
13out2 = f(get_val())
14print('after fix simulation:', out2)
--- orig.py	2026-10-17 16:34:35.071909072 -0400
+++ fixed.py	2026-10-17 16:34:35.237909374 -0400
@@ -7,7 +7,7 @@
 def f(arg):
     return arg, len(calls)
 
-out = f(not (not get_val()))
+out = f(bool(get_val()))
 print('result:', out)
 calls.clear()
 out2 = f(get_val())
result: (False, 1)
after fix simulation: (False, 1)
r1-p006 · Boolean forcing with numpy-like truthiness altered by fix
1class BoolLike:
2 def __init__(self, val):
3 self.val = val
4 def __bool__(self):
5 return bool(self.val)
6 def __repr__(self):
7 return f'BoolLike({self.val})'
8
9x = BoolLike(0)
10forced = not (not x)
11direct = x
12print('forced type:', type(forced), forced)
13print('direct type:', type(direct), direct)
14print('are they same object?', forced is direct)
--- orig.py	2026-10-17 16:34:35.343909568 -0400
+++ fixed.py	2026-10-17 16:34:35.504909861 -0400
@@ -7,7 +7,7 @@
         return f'BoolLike({self.val})'
 
 x = BoolLike(0)
-forced = not (not x)
+forced = bool(x)
 direct = x
 print('forced type:', type(forced), forced)
 print('direct type:', type(direct), direct)
forced type: <class 'bool'> False
direct type: <class '__main__.BoolLike'> BoolLike(0)
are they same object? False
r1-p007 · Class vs instance attribute with property side effect
1class Counter:
2 count = 0
3 @property
4 def value(self):
5 Counter.count += 1
6 return Counter.count > 0
7
8obj = Counter()
9if not (not obj.value):
10 print('first access:', Counter.count)
11else:
12 print('unreachable')
13print('second access:', Counter.count)
--- orig.py	2026-10-17 16:34:35.605910046 -0400
+++ fixed.py	2026-10-17 16:34:35.768910343 -0400
@@ -6,7 +6,7 @@
         return Counter.count > 0
 
 obj = Counter()
-if not (not obj.value):
+if obj.value:
     print('first access:', Counter.count)
 else:
     print('unreachable')
first access: 1
second access: 1
r1-p008 · Star-imports and module-level side effects
1import sys
2mod = sys.modules[__name__]
3if not hasattr(mod, '_init'):
4 mod._init = True
5 mod.state = 0
6
7def side_effect():
8 mod.state += 1
9 return mod.state > 0
10
11if not (not side_effect()):
12 print('state after:', mod.state)
13else:
14 print('unreachable')
15print('final state:', mod.state)
--- orig.py	2026-10-17 16:34:35.869910527 -0400
+++ fixed.py	2026-10-17 16:34:36.034910828 -0400
@@ -8,7 +8,7 @@
     mod.state += 1
     return mod.state > 0
 
-if not (not side_effect()):
+if side_effect():
     print('state after:', mod.state)
 else:
     print('unreachable')
state after: 1
final state: 1
r2-p001 · Double Negation Inside Function Call Alters Evaluation Count
1def f(arg):
2 global counter
3 counter += arg
4 return arg
5counter = 0
6result = f(not (not True))
7expected_calls = 2
8actual_calls = counter
9print(f'{expected_calls=} {actual_calls=}')
--- orig.py	2026-10-17 16:35:52.976062087 -0400
+++ fixed.py	2026-10-17 16:35:53.144062439 -0400
@@ -3,7 +3,7 @@
     counter += arg
     return arg
 counter = 0
-result = f(not (not True))
+result = f(bool(True))
 expected_calls = 2
 actual_calls = counter
 print(f'{expected_calls=} {actual_calls=}')
expected_calls=2 actual_calls=1
r2-p002 · Indexing Operation May Raise IndexError Twice vs Once
1lst = [1] # length 1
2idx_list = []
3def get_idx():
4 idx_list.append(0)
5 val = len(idx_list)
6 lst.clear()
7 return val
8try:
9 result = not (not lst[get_idx()])
10except IndexError as e:
11 passes = len([i for i in range(len(idx_list)) if i < len(lst)])
12 print(f'Exception caught: {type(e).__name__}: {e}, index evaluations: {len(idx_list)}')
13else:
14 print(f'Result: {result}')
--- orig.py	2026-10-17 16:35:53.244062650 -0400
+++ fixed.py	2026-10-17 16:35:53.407062992 -0400
@@ -6,7 +6,7 @@
     lst.clear()
     return val
 try:
-    result = not (not lst[get_idx()])
+    result = bool(lst[get_idx()])
 except IndexError as e:
     passes = len([i for i in range(len(idx_list)) if i < len(lst)])
     print(f'Exception caught: {type(e).__name__}: {e}, index evaluations: {len(idx_list)}')
Exception caught: IndexError: list index out of range, index evaluations: 1
r2-p003 · Conditional Expression Evaluates Condition Twice (generated program does not parse: Traceback (most recent call last):)
1cond = [True]
2def get_cond():
3 val = cond[0]
4 cond.clear()
5 cond.append(False)
6 return val
7branch_taken = None
8x = 1 if get_cond() else 2
9# Simulate what not(not(x)) does internally
10first_eval = branch_taken := (1 if get_cond() else 2)
11second_eval = branch_taken := (1 if get_cond() else 2)
12print(f'First branch eval: {first_eval}, Second branch eval: {second_eval}')
13# Actual code under test
14try:
15 result = not (not (1 if get_cond() else 2))
16except Exception as e:
17 print(f'Error: {e}')
18else:
19 print(f'Result: {result}')
r2-p004 · Comment Between Not Tokens Would Be Lost
1# This line tests comment preservation in double negation
2# Ruff may drop internal comments when fixing not(not x)
3# We cannot observe comment loss directly, so we use a marker
4marker = 'preserved'
5val = not (
6 # comment that should stay
7 not marker
8)
9print(f'Marker value: {marker}, Result: {val}')
--- orig.py	2026-10-17 16:35:53.654063511 -0400
+++ fixed.py	2026-10-17 16:35:53.821063862 -0400
@@ -2,8 +2,5 @@
 # Ruff may drop internal comments when fixing not(not x)
 # We cannot observe comment loss directly, so we use a marker
 marker = 'preserved'
-val = not (
-    # comment that should stay
-    not marker
-)
+val = bool(marker)
 print(f'Marker value: {marker}, Result: {val}')
Marker value: preserved, Result: True
r2-p005 · Pathlib Vs Os.Path Behavior On Relative Paths Etc
1import os
2name = 'testfile'
3with open(name, 'w') as f:
4 f.write('data')
5try:
6 # Using os.path.exists - not(not(...)) form
7 exists = not (not os.path.exists(name))
8 print(f'os.path: {exists}')
9 os.remove(name)
10 # Second check after delete - would differ if duplicated
11 exists2 = not (not os.path.exists(name))
12 print(f'os.path after remove: {exists2}')
13except Exception as e:
14 print(f'OS error: {e}')
15finally:
16 if os.path.exists(name):
17 os.remove(name)
--- orig.py	2026-10-17 16:35:53.921064072 -0400
+++ fixed.py	2026-10-17 16:35:54.104064456 -0400
@@ -4,11 +4,11 @@
     f.write('data')
 try:
     # Using os.path.exists - not(not(...)) form
-    exists = not (not os.path.exists(name))
+    exists = bool(os.path.exists(name))
     print(f'os.path: {exists}')
     os.remove(name)
     # Second check after delete - would differ if duplicated
-    exists2 = not (not os.path.exists(name))
+    exists2 = bool(os.path.exists(name))
     print(f'os.path after remove: {exists2}')
 except Exception as e:
     print(f'OS error: {e}')
os.path: True
os.path after remove: False
r2-p006 · Method Returns Mutable Object That Changes State
1class MutableReturn:
2 def __init__(self):
3 self.state = 'original'
4 def mutate(self):
5 self.state = 'mutated'
6 return self
7class Obj:
8 def __init__(self):
9 self.call_count = 0
10 def method(self):
11 self.call_count += 1
12 return MutableReturn().mutate() if self.call_count == 1 else MutableReturn()
13obj = Obj()
14try:
15 # First call: returns mutated object
16 # Second call: returns fresh object
17 result = not (not obj.method())
18 print(f'Method calls: {obj.call_count}, Result truthiness: {bool(result)}')
19except Exception as e:
20 print(f'Error: {e}')
--- orig.py	2026-10-17 16:35:54.205064669 -0400
+++ fixed.py	2026-10-17 16:35:54.373065023 -0400
@@ -14,7 +14,7 @@
 try:
     # First call: returns mutated object
     # Second call: returns fresh object
-    result = not (not obj.method())
+    result = bool(obj.method())
     print(f'Method calls: {obj.call_count}, Result truthiness: {bool(result)}')
 except Exception as e:
     print(f'Error: {e}')
Method calls: 1, Result truthiness: True
r2-p007 · Property With Side-Effecting __Get__ Accessed Twice
1class SideEffectProp:
2 def __init__(self):
3 self.access_count = 0
4 @property
5 def prop(self):
6 self.access_count += 1
7 return self.access_count == 1 # True only on first access
8obj = SideEffectProp()
9try:
10 # Original: accesses prop twice -> first True, second False -> not(not(...)) = False
11 # Fix: accesses once -> True -> not(False) = True? Wait: not(not(x)) where x=prop
12 # First access: prop=True -> not(True)=False -> not(False)=True
13 # But second access happens in original: prop becomes False on second access
14 # So: not(not(obj.prop)) evaluates obj.prop twice
15 first = obj.prop # access_count=1, returns True
16 second = obj.prop # access_count=2, returns False
17 inner = not second # not False = True
18 result = not inner # not True = False
19 print(f'Access count: {obj.access_count}, Expected result: {result}')
20 # Now test actual expression
21 obj2 = SideEffectProp()
22 actual = not (not obj2.prop)
23 print(f'Actual access count: {obj2.access_count}, Actual result: {actual}')
24except Exception as e:
25 print(f'Error: {e}')
--- orig.py	2026-10-17 16:35:54.475065237 -0400
+++ fixed.py	2026-10-17 16:35:54.641065586 -0400
@@ -19,7 +19,7 @@
     print(f'Access count: {obj.access_count}, Expected result: {result}')
     # Now test actual expression
     obj2 = SideEffectProp()
-    actual = not (not obj2.prop)
+    actual = bool(obj2.prop)
     print(f'Actual access count: {obj2.access_count}, Actual result: {actual}')
 except Exception as e:
     print(f'Error: {e}')
Access count: 2, Expected result: False
Actual access count: 1, Actual result: True
r2-p008 · Keyword-Only Positional-Only *Args/**Kwargs Forwarding Defaults Evaluated Once
1def process(a, /, b, *, c=[]):
2 c.append(a + b)
3 return c
4call_log = []
5def make_k():
6 call_log.append('eval')
7 return [99]
8try:
9 # This will trigger SIM208 on the inner not(not(...) if we had it
10 # Instead we test defaults in context
11 result = process(1, 2, c=not (not make_k()))
12 print(f'Call log length: {len(call_log)}, Result: {result}')
13except Exception as e:
14 print(f'Error: {e}')
--- orig.py	2026-10-17 16:35:54.741065797 -0400
+++ fixed.py	2026-10-17 16:35:54.905066142 -0400
@@ -8,7 +8,7 @@
 try:
     # This will trigger SIM208 on the inner not(not(...) if we had it
     # Instead we test defaults in context
-    result = process(1, 2, c=not (not make_k()))
+    result = process(1, 2, c=bool(make_k()))
     print(f'Call log length: {len(call_log)}, Result: {result}')
 except Exception as e:
     print(f'Error: {e}')
Error: 'bool' object has no attribute 'append'