ruff E714
labelled unknown
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
| program | oracle | label | judge | status | summary |
|---|---|---|---|---|---|
| r1-p007 | fix-changes-behavior | labelled safe | introspection-only · low | related | The 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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Precedence alteration in compound condition | 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) |
| r1-p002 | fix ok prediction off | Type annotation context | type annotations evaluated at runtime, typing-only imports |
| r1-p003 | fix ok | Exception handling interaction | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r1-p004 | invalid | Comment preservation between not and is | Comments 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-p005 | fix ok | Generator consumption difference | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r1-p006 | invalid | Shadowed built-in interaction | shadowed builtins and names redefined later in the module |
| r1-p007 | fix-changes-behavior | Bytes vs str identity | bytes vs str, encoding and newline handling, text vs binary modes |
| r1-p008 | fix ok | Evaluation order with side effects | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p001 | fix ok | Class attribute descriptor interference | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p002 | invalid | Mutable default argument aliasing | mutable default arguments and aliasing after a rewrite |
| r2-p003 | fix ok prediction off | Parenthesized is expression breaks grouping | 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. |
| r2-p004 | fix ok | Line continuation with backslash | 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. |
| r2-p005 | fix ok | String formatting with percent and f-string | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p006 | fix ok | Shadowed built-in redefined later | shadowed builtins and names redefined later in the module |
| r2-p007 | fix ok | Dict ordering with custom hash | dict ordering, duplicate keys, __hash__/__eq__ overrides, sets |
| r2-p008 | fix ok | Evaluation order with side effects in arguments | evaluation 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]