ruff SIM202
labelled unknown
Analysis Nemotron 3 Super
Flags negated inequality expressions (`not x != y`) to suggest replacing them with equality checks (`x == y`).
Fix: Replaces `not X != Y` with `X == Y`, preserving the surrounding expression structure but removing the outer `not` and inverting the inequality operator to equality.
10 hazards
- If `X.__ne__` returns a non-boolean (e.g., int, None), `not X != Y` may be truthy/falsy based on that value, while `X == Y` invokes `__eq__` which may return a different non-boolean, altering truthiness.
- If `X.__eq__` has side effects (e.g., logging, state mutation), the fix introduces those where none occurred before, as the original only evaluated `__ne__`.
- If `X` or `Y` is a property with side effects in its getter, the fix may evaluate it once (for `==`) vs twice (once for `!=` in `not ...`), changing side effect count.
- In expressions like `not x != y if cond else z`, the fix alters grouping: original parses as `not (x != y) if cond else z`, fix produces `x == y if cond else z`, which is equivalent, but if misparsed due to low precedence of `if-else`, could risk misgrouping—though Ruff respects precedence, this notes developer confusion potential.
- If `X` or `Y` overloads `__ne__` to return a custom truthy/falsey object (e.g., always returns `1`), `not X != Y` is `False`, while `X == Y` may invoke `__eq__` returning `0` → still `False`, but if `__eq__` returns `2`, both false; however, if `__ne__` returns `0` (falsey) → `not 0` is `True`, but `__eq__` returns `None` → `not None` is `True` only if fixed to `X == Y` then `not` removed—wait: fix removes `not`, so `X == Y` directly: if `__eq__` returns `None`, it's falsey → `False`, whereas original `not (X != Y)` with `__ne__` returning `0` (falsey) → `not falsey` → `True`. So divergence when `__ne__` returns falsey non-bool and `__eq__` returns falsey non-bool but different falsiness nuance? Actually: original: `not (X != Y)`. If `X != Y` returns `0` → `not 0` → `True`. Fix: `X == Y`. If `X == Y` returns `None` → falsey → `False`. So behavior differs. Hazard: return types of `__ne__`/`__eq__` affect truth value after `not` vs direct eval.
- If `X` is `NaN` (float), `X != X` is `True`, so `not X != X` is `False`; `X == X` is `False` (since `NaN != NaN`), so fix gives `False` → same. But if `__eq__` overridden to return `True` for `NaN`, then fix diverges. Covered by non-boolean return, but worth noting: even boolean-returning overrides can break if they violate reflexivity/symmetry expectations assumed by the fix.
- If the expression contains a `# type: ignore` comment on the `!=` token, the fix may drop it when rewriting the operator, losing suppression.
- If the code relies on the syntactic form `not ... != ...` for searchability or tooling (e.g., regex-based refactors), the fix alters the source pattern, breaking such tools.
- If `X` or `Y` is a tuple/list comparison, `not X != Y` is equivalent to `X == Y` only if `__ne__` and `__eq__` are consistent; but if one is overridden and not the other (e.g., `__ne__` returns `NotImplemented` leading to fallback, `__eq__` overridden), the fix may not mirror fallback behavior.
- In rare cases where `X != Y` raises an exception (e.g., `AttributeError` in `__ne__`), the original `not X != Y` propagates it; the fix `X == Y` may not raise (if `__eq__` handles it) or raise a different exception, altering error flow.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Runtime Type Annotation Evaluation | type annotations evaluated at runtime, typing-only imports |
| r1-p002 | fired, no fix | Parameter Passing Edge Cases | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r1-p003 | fired, no fix | String Formatting Edge Cases | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r1-p004 | fired, no fix | Mutable Default Arguments and Aliasing | mutable default arguments and aliasing after a rewrite |
| r1-p005 | fired, no fix | Inconsistent __eq__ and __ne__ Override | If `X` or `Y` is a tuple/list comparison, `not X != Y` is equivalent to `X == Y` only if `__ne__` and `__eq__` are consistent; but if one is overridden and not the other (e.g., `__ne__` returns `NotIm |
| r1-p006 | fired, no fix | Conditional Expression Grouping Clarity | In expressions like `not x != y if cond else z`, the fix alters grouping: original parses as `not (x != y) if cond else z`, fix produces `x == y if cond else z`, which is equivalent, but if misparsed |
| r1-p007 | fired, no fix | Property Getter Side Effects | If `X` or `Y` is a property with side effects in its getter, the fix may evaluate it once (for `==`) vs twice (once for `!=` in `not ...`), changing side effect count. |
| r1-p008 | fired, no fix | File Modes and Truncation | file modes and truncation, partial writes, non-str data passed to write |
| r2-p001 | fired, no fix | Precedence interaction with conditional expression | In expressions like `not x != y if cond else z`, the fix alters grouping: original parses as `not (x != y) if cond else z`, fix produces `x == y if cond else z`, which is equivalent, but if misparsed |
| r2-p002 | fired, no fix | __ne__ returns non-boolean affecting truthiness under not | If `X.__ne__` returns a non-boolean (e.g., int, None), `not X != Y` may be truthy/falsy based on that value, while `X == Y` invokes `__eq__` which may return a different non-boolean, altering truthine |
| r2-p003 | silent prediction off | Syntactic form relied upon for tooling/regex | If the code relies on the syntactic form `not ... != ...` for searchability or tooling (e.g., regex-based refactors), the fix alters the source pattern, breaking such tools. |
| r2-p004 | fired, no fix | bytes vs str, encoding and newline handling | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p005 | fired, no fix | context managers that are not files, objects with write() but no close() | context managers that are not files, objects with write() but no close() |
| r2-p006 | fired, no fix | __ne__ returns falsey non-bool, __eq__ returns different falsey non-bool | If `X` or `Y` overloads `__ne__` to return a custom truthy/falsey object (e.g., always returns `1`), `not X != Y` is `False`, while `X == Y` may invoke `__eq__` returning `0` → still `False`, but if ` |
| r2-p007 | fired, no fix | NaN with overridden __eq__ returning True | If `X` is `NaN` (float), `X != X` is `True`, so `not X != X` is `False`; `X == X` is `False` (since `NaN != NaN`), so fix gives `False` → same. But if `__eq__` overridden to return `True` for `NaN`, t |
| r2-p008 | fired, no fix | Custom __bool__ and len, None vs falsy, 'is' vs '==' | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
Sources
r1-p001 · Runtime Type Annotation Evaluation
1import typing
2T = int
3def f(x: 'T') -> None:
4 val = not x != 5
5 print('val:', val)
6f(5)
val: True
r1-p002 · Parameter Passing Edge Cases
1def g(a, /, b, *, c=not 1 != 1, d):
2 print(c, d)
3g(1, 2, d=3)
True 3
r1-p003 · String Formatting Edge Cases
1name = 'world'
2msg = f'Hello {not "hi" != "hi"} {name}'
3print(msg)
4print('%s' % (not 'a' != 'a'))
Hello True world True
r1-p004 · Mutable Default Arguments and Aliasing
1def append_val(x, lst=[]):
2 if not x != 5:
3 lst.append(x)
4 return lst
5print(append_val(5))
6print(append_val(5))
[5] [5, 5]
r1-p005 · Inconsistent __eq__ and __ne__ Override
1class A:
2 def __eq__(self, other):
3 return True
4 def __ne__(self, other):
5 return NotImplemented
6class B:
7 pass
8a = A()
9b = B()
10print('not a != b:', not a != b)
11print('a == b:', a == b)
not a != b: False a == b: True
r1-p006 · Conditional Expression Grouping Clarity
1x = 5
2y = 5
3z = 0
4result = not x != y if True else z
5print('result:', result)
result: True
r1-p007 · Property Getter Side Effects
1class Counter:
2 def __init__(self):
3 self.count = 0
4 @property
5 def value(self):
6 self.count += 1
7 return 5
8c = Counter()
9print('before:', c.count)
10expr = not c.value != 5
11print('after:', c.count, 'expr:', expr)
before: 0 after: 1 expr: True
r1-p008 · File Modes and Truncation
1with open('test.txt', 'w') as f:
2 if not '' != '':
3 f.write('hello')
4with open('test.txt', 'r') as f:
5 print(f.read())
hello
r2-p001 · Precedence interaction with conditional expression
1def f():
2 x, y = 1, 2
3 cond = True
4 z = 99
5 result = (not x != y) if cond else z
6 print('orig:', result)
7 # After fix: becomes x == y if cond else z
8 result_fixed = (x == y) if cond else z
9 print('fixed:', result_fixed)
10 assert result == result_fixed
11f()
orig: False fixed: False
r2-p002 · __ne__ returns non-boolean affecting truthiness under not
1class A:
2 def __ne__(self, other):
3 return 0 # falsey
4 def __eq__(self, other):
5 return None # falsey
6
7a = A()
8b = A()
9print('not a != b:', not a != b)
10print('a == b:', a == b)
11# After fix: not a != b -> a == b
12print('Fixed value (a == b):', a == b)
13# Original was True, fixed is False -> divergence
14assert not a != b == True
15assert a == b == None
16print('Divergence detected')
not a != b: True a == b: None Fixed value (a == b): None
r2-p003 · Syntactic form relied upon for tooling/regex
1import re
2pattern = r'not\s+.+!='
3source = '''not a != b
4not(x != y)
5'''
6matches = re.findall(pattern, source)
7print('Matches before fix:', len(matches))
8# Simulate after fix: replace not X != Y with X == Y
9source_fixed = re.sub(r'not\s+\((.+?)!=(.+?)\)', r'\1 == \2', source)
10source_fixed = re.sub(r'not\s+(.+?)!=(.+)', r'\1 == \2', source_fixed)
11matches_after = re.findall(pattern, source_fixed)
12print('Matches after fix:', len(matches_after))
13print('Change in matches:', len(matches) - len(matches_after))
Matches before fix: 1 Matches after fix: 0 Change in matches: 1
r2-p004 · bytes vs str, encoding and newline handling
1b1 = b'hello\n'
2b2 = b'hello\n'
3s1 = 'hello\n'
4s2 = 'hello\n'
5print('not b1 != b2:', not b1 != b2)
6print('b1 == b2:', b1 == b2)
7print('not s1 != s2:', not s1 != s2)
8print('s1 == s2:', s1 == s2)
9# After fix: not x != y -> x == y
10print('Fixed b comparison:', b1 == b2)
11print('Fixed s comparison:', s1 == s2)
12# Same here, but show we checked
13assert (not b1 != b2) == (b1 == b2)
14assert (not s1 != s2) == (s1 == s2)
15print('Byte/str comparisons consistent')
not b1 != b2: True b1 == b2: True not s1 != s2: True s1 == s2: True Fixed b comparison: True Fixed s comparison: True Byte/str comparisons consistent
r2-p005 · context managers that are not files, objects with write() but no close()
1class SilentWriter:
2 def __init__(self):
3 self.data = ''
4 def write(self, s):
5 self.data += s
6 def __enter__(self):
7 return self
8 def __exit__(self, *args):
9 pass
10
11w = SilentWriter()
12with w as f:
13 f.write('test')
14# Simulate a comparison involving the object
15class CmpObj:
16 def __init__(self, val):
17 self.val = val
18 def __ne__(self, other):
19 return self.val != other.val
20 def __eq__(self, other):
21 return self.val == other.val
22
23a = CmpObj(1)
24b = CmpObj(1)
25print('not a != b:', not a != b)
26print('After fix (a == b):', a == b)
27assert (not a != b) == (a == b)
28print('Context manager and comparison stable')
not a != b: True After fix (a == b): True Context manager and comparison stable
r2-p006 · __ne__ returns falsey non-bool, __eq__ returns different falsey non-bool
1class X:
2 def __ne__(self, other):
3 return 0 # falsey -> not 0 = True
4 def __eq__(self, other):
5 return None # falsey, but when used directly -> False
6
7class Y:
8 pass
9
10x = X()
11y = Y()
12orig = not x != y
13fixed = x == y
14print('not x != y:', orig)
15print('x == y:', fixed)
16print('Types:', type(orig), type(fixed))
17if orig != fixed:
18 print('Divergence: original', orig, 'vs fixed', fixed)
19else:
20 print('No divergence')
not x != y: True x == y: None Types: <class 'bool'> <class 'NoneType'> Divergence: original True vs fixed None
r2-p007 · NaN with overridden __eq__ returning True
1class Nanoid(float):
2 def __eq__(self, other):
3 return True # violates NaN expectations
4 def __ne__(self, other):
5 return not super().__eq__(other)
6
7x = Nanoid(float('nan'))
8print('x != x:', x != x)
9print('not x != x:', not x != x)
10print('x == x:', x == x)
11# Original: not (x != x) -> not True -> False
12# Fixed: x == x -> True
13orig = not x != x
14fixed = x == x
15print('Original result:', orig)
16print('Fixed result:', fixed)
17if orig != fixed:
18 print('Divergence due to __eq__ override')
19else:
20 print('Same result')
x != x: True not x != x: False x == x: True Original result: False Fixed result: True Divergence due to __eq__ override
r2-p008 · Custom __bool__ and len, None vs falsy, 'is' vs '=='
1class EmptyListLike:
2 def __len__(self):
3 return 0
4 def __bool__(self):
5 return False
6 def __ne__(self, other):
7 return len(self) != len(other) if hasattr(other, '__len__') else True
8 def __eq__(self, other):
9 return len(self) == len(other) if hasattr(other, '__len__') else False
10
11a = EmptyListLike()
12b = EmptyListLike()
13c = []
14print('not a != b:', not a != b)
15print('a == b:', a == b)
16print('not a != c:', not a != c)
17print('a == c:', a == c)
18print('a is []:', a is [])
19print('bool(a):', bool(a))
20# After fix: not x != y -> x == y
21print('Fixed a==b:', a == b)
22print('Fixed a==c:', a == c)
23assert (not a != b) == (a == b)
24assert (not a != c) == (a == c)
25print('Custom bool/len comparisons consistent')
not a != b: True a == b: True not a != c: True a == c: True a is []: False bool(a): False Fixed a==b: True Fixed a==c: True Custom bool/len comparisons consistent