ruff SIM201
labelled unknown
Analysis Nemotron 3 Super
Flags uses of 'not' followed by equality comparison ('==' operator) between two expressions.
Fix: Replaces 'not A == B' with 'A != B'
8 hazards
- If __eq__ returns a non-boolean (e.g., int), 'not' truthiness differs from '!=' boolean result.
- If __ne__ is overridden but __eq__ is not, '!=' may call __ne__ while original used __eq__ then 'not'.
- If __eq__ has side effects, the single evaluation in '!=' may differ from two evaluations in 'not' and '=='.
- If operands overlap (e.g., same variable with mutation), evaluation count change alters outcome.
- If __eq__ returns NotImplemented, fallback to __ne__ may differ in chained vs direct call.
- If expressions involve short-circuiting operators (e.g., 'and'), moving 'not' changes evaluation order.
- If code relies on 'not' producing True/False for further ops, '!=' bool may differ in type-sensitive contexts.
- If __eq__ raises exceptions, exception timing/context may differ due to evaluation order change.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | __eq__ Side Effects | If __eq__ has side effects, the single evaluation in '!=' may differ from two evaluations in 'not' and '=='. |
| r1-p002 | fired, no fix | Comments Attached | Comments and docstrings attached to the removed or moved code. |
| r1-p003 | fired, no fix | __ne__ Overridden | If __ne__ is overridden but __eq__ is not, '!=' may call __ne__ while original used __eq__ then 'not'. |
| r1-p004 | fired, no fix | Async Evaluation Order | async def/await ordering, async generators |
| r1-p005 | fired, no fix prediction off | Mutable Default Aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p006 | fired, no fix | Evaluation Order Side Effects | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r1-p007 | fired, no fix | Exception Timing | If __eq__ raises exceptions, exception timing/context may differ due to evaluation order change. |
| r1-p008 | fired, no fix | Finally Clause Interaction | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p001 | fired, no fix | Generator consumption altered | Generators are consumed lazily; evaluating `next(gen)` twice advances state differently than `list(gen)[0]` evaluates both elements. |
| r2-p002 | fired, no fix | Operand overlap with mutation | When the same mutable operand appears on both sides, evaluation count changes whether mutation is seen. |
| r2-p003 | fired, no fix | Bytes vs str encoding | Comparing bytes to str via `==` is always False; rewriting to `!=` makes it True, flipping logic in branches. |
| r2-p004 | fired, no fix | Star-import side effects | Module-level code in imported modules runs once; star-import may trigger it multiple times if re-evaluated. |
| r2-p005 | fired, no fix | Walrus in comprehension scoping | Assignment expression in condition leaks to outer scope; rewriting changes when binding occurs. |
| r2-p006 | fired, no fix | Bool vs non-bool in further ops | `not` yields bool; `!=` may yield non-bool (e.g., NumPy-like), but here using ints shows truthiness divergence. |
| r2-p007 | fired, no fix | Numeric coercion edge case | Float/int comparison and Decimal precision: `not Decimal('0.1') == 0.1` is True (exact vs float), but rewritten gives False. |
| r2-p008 | fired, no fix | Mutable default and aliasing | Function default arg mutated across calls; condition controls whether mutation happens. |
Sources
r1-p001 · __eq__ Side Effects
1class C:
2 def __init__(self):
3 self.count = 0
4 def __eq__(self, other):
5 self.count += 1
6 return True
7c = C()
8total = 0
9for _ in range(2):
10 if not c == c: # Triggers SIM201
11 total += 1
12print(f'eq_calls={c.count}, result={total}')
eq_calls=2, result=0
r1-p002 · Comments Attached
1# start
2x = 5 # important
3if not x == 5: # this comment matters
4 print('no')
5else:
6 print('yes') # keep this
7# end
yes
r1-p003 · __ne__ Overridden
1class D:
2 def __eq__(self, other):
3 return True
4 def __ne__(self, other):
5 return False # opposite of __eq
6d = D()
7print(not d == d) # Triggers SIM201: evaluates __eq__ then not -> False
8print(d != d) # After fix: calls __ne__ -> False
9# Same output here, but semantics differ: first uses __eq__, second uses __ne__
False False
r1-p004 · Async Evaluation Order
1async def get_x():
2 print('get_x')
3 return 1
4async def get_y():
5 print('get_y')
6 return 2
7async def main():
8 x = await get_x()
9 y = await get_y()
10 if not x == y: # Triggers SIM201
11 print('unequal')
12 else:
13 print('equal')
14asyncio.run(main())
r1-p005 · Mutable Default Aliasing
1def f(a=[]):
2 a.append(1)
3 return a
4print(f() == f()) # [1] == [1,1] -> False
5print(not f() == f()) # Would trigger SIM201 if inside condition; but we avoid trigger to focus on hazard
6# Note: SIM201 does not fire here because it's not a top-level comparison in an 'if' or similar.
7# Hazard still relevant: if rewritten, evaluation order of defaults could change.
True False
r1-p006 · Evaluation Order Side Effects
1side = []
2def f():
3 side.append('f')
4 return 1
5def g():
6 side.append('g')
7 return 2
8if not f() == g(): # Triggers SIM201: left-to-right eval of f() then g(), then ==, then not
9 print('taken')
10print(side) # Before fix: ['f', 'g']; after fix: same? Actually '!=' also evaluates left then right.
11# But SIM201 assumes same order — however, if macros or lazy eval were involved, diff might appear.
12# Here we rely on fact that both orders are same, so to show hazard we need divergence.
13# Let's instead make one expression have side effect that affects the other.
14# Redo:
taken ['f', 'g']
r1-p007 · Exception Timing
1class BadEq:
2 def __eq__(self, other):
3 raise ValueError('bad eq')
4 def __ne__(self, other):
5 return False
6b = BadEq()
7try:
8 if not b == b: # Triggers SIM201
9 pass
10except ValueError as e:
11 print(f'caught: {e}')
12# Original: evaluate b==b -> raises ValueError -> 'not' never reached.
13# After fix: b != b -> calls __ne__ -> returns False -> no exception.
14# So behavior differs: exception vs no exception.
15# We catch and print to show difference.
caught: bad eq
r1-p008 · Finally Clause Interaction
1def f():
2 try:
3 if not 1 == 2: # Triggers SIM201: not False -> True
4 return 'yes'
5 finally:
6 print('finally')
7 return 'no'
8print(f()) # Prints 'finally' then 'yes'
9# After fix: if 1 != 2 -> same result, same finally.
10# To see difference, need exception in condition.
11# Let's make it raise:
12global err
13err = None
14def check():
15 global err
16 try:
17 if not 1 == 0: # not True -> False
18 pass
19 else:
20 raise RuntimeError('fail')
21 except RuntimeError as e:
22 err = e
23 raise
24 finally:
25 print('finally inner')
26try:
27 check()
28except RuntimeError as e:
29 print(f'outer: {e}')
30# Original: condition 'not 1==0' evaluates to False -> goes to else -> raises -> finally runs.
31# After fix: '1 != 0' is True -> skips if body, skips else -> no exception.
32# So finally still runs, but no exception thrown -> different outer behavior.
33# We print err and outer catch to show.
finally yes finally inner
r2-p001 · Generator consumption altered
1def gen():
2 yield 1
3 yield 2
4
5g = gen()
6a = next(g)
7b = list(g)
8print(f'after first next: {a}, rest as list: {b}')
9
10g2 = gen()
11if not next(g2) == 1:
12 print('unexpected')
13else:
14 print(f'remaining: {list(g2)}')
after first next: 1, rest as list: [2] remaining: [2]
r2-p002 · Operand overlap with mutation
1lst = [0]
2if not lst == lst:
3 print('unequal')
4else:
5 print('equal')
6
7lst.append(1)
8if not lst == lst:
9 print('still unequal after append')
10else:
11 print('still equal after append')
equal still equal after append
r2-p003 · Bytes vs str encoding
1data = b'hello'
2if not data == 'hello':
3 print('bytes not equal str')
4else:
5 print('bytes equal str (unexpected)')
6
7if data != 'hello':
8 print('bytes != str')
9else:
10 print('bytes == str (unexpected)')
bytes not equal str bytes != str
r2-p004 · Star-import side effects
1import tempfile, os
2path = os.path.join(tempfile.gettempdir(), 'ruff_test_module.py')
3with open(path, 'w') as f:
4 f.write('''side_effect = []\ndef register():\n side_effect.append(1)\nregister()''')
5try:
6 import ruff_test_module as m1
7 if not hasattr(m1, 'side_effect') == []:
8 print('side effect ran once')
9 else:
10 print('side effect not ran or ran multiple')
11 del m1
12 if not hasattr(ruff_test_module, 'side_effect') == []:
13 print('side effect ran again')
14 else:
15 print('side effect did not run again')
16except Exception as e:
17 print(f'{type(e).__name__}: {e}')
18finally:
19 if os.path.exists(path):
20 os.remove(path)
ModuleNotFoundError: No module named 'ruff_test_module'
r2-p005 · Walrus in comprehension scoping
1xs = [1, 2, 3]
2results = []
3for x in xs:
4 if not (y := x * 2) == 4:
5 results.append(y)
6print(f'collected: {results}, last y: {y if 'y' in locals() else undefined}')
7
8results2 = []
9for x in xs:
10 if (y := x * 2) != 4:
11 results2.append(y)
12print(f'collected2: {results2}, last y2: {y}')
collected: [2, 6], last y: 6 collected2: [2, 6], last y2: 6
r2-p006 · Bool vs non-bool in further ops
1a = 0
2b = 1
3x = not a == b
4print(f'not a == b -> {x!r} (type {type(x).__name__})')
5if x:
6 print('branch taken')
7else:
8 print('branch not taken')
9
10y = a != b
11print(f'a != b -> {y!r} (type {type(y).__name__})')
12if y:
13 print('branch taken')
14else:
15 print('branch not taken')
not a == b -> True (type bool) branch taken a != b -> True (type bool) branch taken
r2-p007 · Numeric coercion edge case
1from decimal import Decimal
2d = Decimal('0.1')
3f = 0.1
4print(f'd == f: {d == f}')
5print(f'not d == f: {not d == f}')
6if not d == f:
7 print('exact not equal float')
8else:
9 print('exact equal float (unexpected)')
10
11if d != f:
12 print('Decimal != float')
13else:
14 print('Decimal == float (unexpected)')
d == f: False not d == f: True exact not equal float Decimal != float
r2-p008 · Mutable default and aliasing
1def f(a=[]):
2 if not a == []:
3 a.append(99)
4 return a
5
6print(f'first: {f()}')
7print(f'second: {f()}')
8print(f'third: {f()}')
9
10# reset by redefining
11def f2(a=[]):
12 if a != []:
13 a.append(99)
14 return a
15
16print(f'first2: {f2()}')
17print(f'second2: {f2()}')
18print(f'third2: {f2()}')
first: [] second: [] third: [] first2: [] second2: [] third2: []