fixhunt

ruff E714

labelled unknown
Runs / 2026-10-17T20-49-21-682Z-c4qe-ruff-E714ruff 0.17.12026-10-17 20:49 UTC8 programs × 2 round(s)16 programs, 13 fired, 13 fixed, 1 findings, 3 rejectedmodel cost $0.02

Analysis Nemotron 3 Super

Flags uses of 'not' followed by an identity comparison ('is') where PEP 8 recommends using the 'is not' operator instead.

Fix: Rewrites 'not {left} is {right}' to '{left} is not {right}' by moving the 'not' keyword to between 'is' and the right operand, preserving all other structure and whitespace.

10 hazards
  • The rewrite fails when the 'is' expression is parenthesized (e.g., 'not (x is y)'), as removing 'not' from before the parentheses breaks the grouping and changes meaning.
  • When 'not x is y' appears in a compound condition like 'not x is y and z', the rewrite 'x is not y and z' alters precedence, making it equivalent to '(x is not y) and z' instead of 'not (x is y and z)'.
  • In 'not x is y or z', the rewrite becomes 'x is not y or z', which evaluates as '(x is not y) or z' rather than 'not (x is y or z)', changing logic due to operator precedence.
  • If the identity comparison is part of a larger expression where 'not' applies to more than just the 'is' (e.g., 'not f() is g()'), the rewrite incorrectly scopes the negation only to the 'is' operation, ignoring potential side effects in function calls.
  • The fix assumes 'is' is always an identity operator; if overloaded via __eq__ or __ne__ in a custom class (though rare and discouraged), the semantics of 'is not' vs 'not ... is' could diverge, though 'is' cannot be overloaded in Python.
  • Comments placed between 'not' and the 'is' keyword (e.g., 'not # comment x is y') would be lost or misplaced during the rewrite, as the tool may not preserve interleaved whitespace/comments.
  • In lambda expressions like 'lambda: not x is y', the rewrite to 'lambda: x is not y' is safe, but if the lambda is used in a context where the original form relied on low precedence of 'not', the change could alter behavior when composed with other operators.
  • In comprehensions such as '[expr for expr in items if not expr is skip]', moving 'not' after 'is' is safe locally, but if the comprehension is nested or part of a larger expression, the altered precedence might interact unexpectedly with surrounding logic.
  • The rewrite does not account for line continuation or backslash-joined lines; if 'not x is\n y' is split across lines, the fix may incorrectly place 'is not' or break the line continuation.
  • If the code contains a syntax error or invalid token near the 'not is' pattern (e.g., due to a typo), the autofix might produce invalid syntax or crash during application.

Findings

programoraclelabeljudgestatussummary
r1-p007fix-changes-behaviorlabelled safeintrospection-only · lowrelatedThe rewrite changes `not b is b'hello'` to `b is not b'hello'`, which is semantically equivalent. The only observed difference is the SyntaxWarning message tex…

Programs

idresulttitlehazard
r1-p001fix okPrecedence alteration in compound conditionWhen 'not x is y' appears in a compound condition like 'not x is y and z', the rewrite 'x is not y and z' alters precedence, making it equivalent to '(x is not y) and z' instead of 'not (x is y and z)
r1-p002fix ok prediction offType annotation contexttype annotations evaluated at runtime, typing-only imports
r1-p003fix okException handling interactionexceptions raised inside the rewritten code, finally/else clauses, exception chaining
r1-p004invalidComment preservation between not and isComments placed between 'not' and the 'is' keyword (e.g., 'not # comment\n x is y') would be lost or misplaced during the rewrite, as the tool may not preserve interleaved whitespace/comments.
r1-p005fix okGenerator consumption differencegenerators vs lists, iterators consumed once, lazy vs eager evaluation
r1-p006invalidShadowed built-in interactionshadowed builtins and names redefined later in the module
r1-p007fix-changes-behaviorBytes vs str identitybytes vs str, encoding and newline handling, text vs binary modes
r1-p008fix okEvaluation order with side effectsevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p001fix okClass attribute descriptor interferenceclass attributes vs instance attributes, descriptors, properties with side effects
r2-p002invalidMutable default argument aliasingmutable default arguments and aliasing after a rewrite
r2-p003fix ok prediction offParenthesized is expression breaks groupingThe rewrite fails when the 'is' expression is parenthesized (e.g., 'not (x is y)'), as removing 'not' from before the parentheses breaks the grouping and changes meaning.
r2-p004fix okLine continuation with backslashThe rewrite does not account for line continuation or backslash-joined lines; if 'not x is\n y' is split across lines, the fix may incorrectly place 'is not' or break the line continuation.
r2-p005fix okString formatting with percent and f-stringstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p006fix okShadowed built-in redefined latershadowed builtins and names redefined later in the module
r2-p007fix okDict ordering with custom hashdict ordering, duplicate keys, __hash__/__eq__ overrides, sets
r2-p008fix okEvaluation order with side effects in argumentsevaluation order and side effects when expressions are merged, duplicated or deleted

Sources

r1-p001 · Precedence alteration in compound condition
1def f():
2 x, y, z = [], [], True
3 r1 = not x is y and z
4 r2 = x is not y and z
5 print('Original:', r1)
6 print('Rewritten form evaluates to:', r2)
--- orig.py	2026-10-17 16:49:33.431586635 -0400
+++ fixed.py	2026-10-17 16:49:33.599590063 -0400
@@ -1,6 +1,6 @@
 def f():
     x, y, z = [], [], True
-    r1 = not x is y and z
+    r1 = x is not y and z
     r2 = x is not y and z
     print('Original:', r1)
     print('Rewritten form evaluates to:', r2)
r1-p002 · Type annotation context
1from typing import List
2
3def foo(x: List[int]) -> bool:
4 return not x is None
5
6print(foo([1]))
--- orig.py	2026-10-17 16:49:33.703592185 -0400
+++ fixed.py	2026-10-17 16:49:33.889595979 -0400
@@ -1,6 +1,6 @@
 from typing import List
 
 def foo(x: List[int]) -> bool:
-    return not x is None
+    return x is not None
 
 print(foo([1]))
True
r1-p003 · Exception handling interaction
1def get_x():
2 raise ValueError('bad')
3
4try:
5 if not get_x() is None:
6 pass
7except ValueError as e:
8 print('Caught:', type(e).__name__, str(e))
--- orig.py	2026-10-17 16:49:34.002598285 -0400
+++ fixed.py	2026-10-17 16:49:34.178601872 -0400
@@ -2,7 +2,7 @@
     raise ValueError('bad')
 
 try:
-    if not get_x() is None:
+    if get_x() is not None:
         pass
 except ValueError as e:
     print('Caught:', type(e).__name__, str(e))
Caught: ValueError bad
r1-p004 · Comment preservation between not and is (generated program does not parse: Traceback (most recent call last):)
1x, y = 1, 2
2if not # this comment should stay
3 x is y:
4 print('equal')
5else:
6 print('not equal')
r1-p005 · Generator consumption difference
1def gen():
2 yield 1
3 yield 2
4
5g = gen()
6print('First check:', not next(g) is None)
7print('Second check:', not next(g) is None)
--- orig.py	2026-10-17 16:49:34.425606906 -0400
+++ fixed.py	2026-10-17 16:49:34.589610248 -0400
@@ -3,5 +3,5 @@
     yield 2
 
 g = gen()
-print('First check:', not next(g) is None)
-print('Second check:', not next(g) is None)
+print('First check:', next(g) is not None)
+print('Second check:', next(g) is not None)
First check: True
Second check: True
r1-p006 · Shadowed built-in interaction (generated program does not parse: Traceback (most recent call last):)
1is = lambda a, b: a == b # shadow built-in 'is'
2x, y = 1, 1
3result = not x is y
4print('Result with shadowed is:', result)
5del is # restore built-in
6result2 = not x is y
7print('Result after restore:', result2)
r1-p007 · Bytes vs str identity
1b = b'hello'
2s = 'hello'
3print('Bytes check:', not b is b'hello')
4print('Str check:', not s is 'hello')
5print('Cross-type:', not b is s)
--- orig.py	2026-10-17 16:49:34.829615139 -0400
+++ fixed.py	2026-10-17 16:49:35.016618951 -0400
@@ -1,5 +1,5 @@
 b = b'hello'
 s = 'hello'
-print('Bytes check:', not b is b'hello')
-print('Str check:', not s is 'hello')
-print('Cross-type:', not b is s)
+print('Bytes check:', b is not b'hello')
+print('Str check:', s is not 'hello')
+print('Cross-type:', b is not s)
Bytes check: False
Str check: False
Cross-type: True
r1-p008 · Evaluation order with side effects
1def side():
2 print('side effect')
3 return []
4
5x = side()
6y = side()
7print('Step 1 done')
8result = not x is y
9print('Result:', result)
--- orig.py	2026-10-17 16:49:35.133621332 -0400
+++ fixed.py	2026-10-17 16:49:35.298624692 -0400
@@ -5,5 +5,5 @@
 x = side()
 y = side()
 print('Step 1 done')
-result = not x is y
+result = x is not y
 print('Result:', result)
side effect
side effect
Step 1 done
Result: True
r2-p001 · Class attribute descriptor interference
1class C:
2 def __get__(self, obj, owner):
3 print('get')
4 return 42
5 x = __get__()
6obj = C()
7if not obj.x is 42:
8 print('fail')
9else:
10 print('pass')
--- orig.py	2026-10-17 16:49:42.170764127 -0400
+++ fixed.py	2026-10-17 16:49:42.560772009 -0400
@@ -4,7 +4,7 @@
         return 42
     x = __get__()
 obj = C()
-if not obj.x is 42:
+if obj.x is not 42:
     print('fail')
 else:
     print('pass')
r2-p002 · Mutable default argument aliasing (generated program is not deterministic)
1def f(a=[]):
2 a.append(1)
3 return a
4x = f()
5if not x is f():
6 print('different')
7else:
8 print('same')
9print(id(x), id(f()))
r2-p003 · Parenthesized is expression breaks grouping
1x = 1
2y = 2
3z = 3
4if not (x is y) and z:
5 print('true')
6else:
7 print('false')
8print(z)
--- orig.py	2026-10-17 16:49:42.910779083 -0400
+++ fixed.py	2026-10-17 16:49:43.074782396 -0400
@@ -1,7 +1,7 @@
 x = 1
 y = 2
 z = 3
-if not (x is y) and z:
+if x is not y and z:
     print('true')
 else:
     print('false')
true
3
r2-p004 · Line continuation with backslash
1x = 1
2y = 2
3if not x is\
4 y:
5 print('equal')
6else:
7 print('not equal')
--- orig.py	2026-10-17 16:49:43.173784395 -0400
+++ fixed.py	2026-10-17 16:49:43.337787706 -0400
@@ -1,7 +1,6 @@
 x = 1
 y = 2
-if not x is\
- y:
+if x is not y:
     print('equal')
 else:
     print('not equal')
equal
r2-p005 · String formatting with percent and f-string
1x = 'hello'
2y = 'hello'
3if not x is y:
4 print('%s != %s' % (x, y))
5else:
6 print(f'{x} is {y}')
--- orig.py	2026-10-17 16:49:43.432789624 -0400
+++ fixed.py	2026-10-17 16:49:43.597792955 -0400
@@ -1,6 +1,6 @@
 x = 'hello'
 y = 'hello'
-if not x is y:
+if x is not y:
     print('%s != %s' % (x, y))
 else:
     print(f'{x} is {y}')
hello is hello
r2-p006 · Shadowed built-in redefined later
1list = []
2list.append(1)
3x = list
4if not x is list:
5 print('aliased')
6else:
7 print('same')
8del list
--- orig.py	2026-10-17 16:49:43.696794953 -0400
+++ fixed.py	2026-10-17 16:49:43.858798224 -0400
@@ -1,7 +1,7 @@
 list = []
 list.append(1)
 x = list
-if not x is list:
+if x is not list:
     print('aliased')
 else:
     print('same')
same
r2-p007 · Dict ordering with custom hash
1class A:
2 def __init__(self, v):
3 self.v = v
4 def __hash__(self):
5 return hash(self.v)
6 def __eq__(self, other):
7 return self.v == other.v
8a = A(1)
9b = A(1)
10d = {a: 'x'}
11if not d.get(b) is None:
12 print('found:', d[b])
13else:
14 print('missing')
--- orig.py	2026-10-17 16:49:43.964800364 -0400
+++ fixed.py	2026-10-17 16:49:44.132803754 -0400
@@ -8,7 +8,7 @@
 a = A(1)
 b = A(1)
 d = {a: 'x'}
-if not d.get(b) is None:
+if d.get(b) is not None:
     print('found:', d[b])
 else:
     print('missing')
found: x
r2-p008 · Evaluation order with side effects in arguments
1ref = []
2def f():
3 ref.append(1)
4 return ref
5x = f()
6if not x is f():
7 print('different call')
8else:
9 print('same call')
10print('ref:', ref)
--- orig.py	2026-10-17 16:49:44.237805871 -0400
+++ fixed.py	2026-10-17 16:49:44.408809320 -0400
@@ -3,7 +3,7 @@
     ref.append(1)
     return ref
 x = f()
-if not x is f():
+if x is not f():
     print('different call')
 else:
     print('same call')
same call
ref: [1, 1]