ruff E711
labelled unknown
Analysis Nemotron 3 Super
Flags non-is comparison operations against None.
Fix: Replaces '==' with 'is' and '!=' with 'is not' when comparing against None literal.
12 hazards
- If __eq__ is overridden, 'x == None' may differ from 'x is None', changing truth value.
- If __ne__ is overridden, 'x != None' may differ from 'x is not None', altering logic flow.
- Rewriting 'None == x' to 'None is x' fails if x defines __eq__ but not symmetric behavior via __eq__ delegation.
- Comments attached to the operator (e.g., 'x == # comment\nNone') may be lost or misplaced during token replacement.
- In expressions like 'f(x == None)', rewriting changes operand type from bool to NoneType, breaking function expectations.
- If None is shadowed locally (e.g., 'None = 5'), the fix incorrectly rewrites to 'is 5' or 'is not 5', causing NameError or wrong semantics.
- In default arguments like 'def f(x=None): if x == None:', the fix alters comparison but not the default, creating inconsistency if None is rebound.
- When used with Optional types in generics, 'x == None' might rely on overloaded equality in proxies, which 'is' bypasses.
- In NumPy arrays, 'arr == None' returns an array of bools, while 'arr is None' is always False — rewrite breaks vectorized intent.
- If the comparison is part of a larger expression like '(x == None) and y', rewriting may short-circuit differently due to truth value of None vs bool.
- In eval() or exec() contexts, the rewritten string may fail if the local scope doesn't contain the built-in None after rebinding.
- If the code relies on the == operator returning a non-boolean (e.g., custom proxy), replacing it with 'is' forces a bool, breaking downstream usage.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Mutable Default Argument Aliasing After Rewrite | Rewriting `x == None` to `x is None` in a mutable default argument context breaks aliasing checks due to shared mutable state across calls. |
| r1-p002 | fired, no fix | Function Expecting Boolean from Equality Comparison | Rewriting `x == None` to `x is None` inside a function call changes the operand from bool to NoneType, breaking functions that expect boolean input. |
| r1-p003 | fired, no fix | Bytes vs Str Encoding and Newline Handling | Rewriting `data == None` in binary/text I/O context can mask encoding-aware checks, leading to silent data corruption when comparing byte/str literals. |
| r1-p004 | fired, no fix | Optional Types and Overloaded Equality in Proxies | Rewriting `x == None` to `x is None` bypasses `__eq__` overloads in proxy objects (e.g., Optional wrappers), breaking lazy validation or logging. |
| r1-p005 | fired, no fix | Comments and Docstrings Attached to Removed Code | Rewriting removes the `== None` expression, taking its inline comment with it and losing documentation about why the check exists. |
| r1-p006 | fired, no fix | Star-Imports, Module-Level Side Effects, __all__ | Rewriting `x == None` in module init changes timing of side effects if the variable is defined via star-import and __all__ affects visibility. |
| r1-p007 | invalid | Generators vs Lists: Iterators Consumed Once | Rewriting `x == None` in a generator expression changes when the check occurs, causing premature consumption or mismatch in lazy evaluation. |
| r1-p008 | fired, no fix | Class Attributes vs Instance Attributes, Descriptors, Properties | Rewriting `self.x == None` to `self.x is None` bypasses `__get__` descriptors, breaking computed or validated attributes. |
| r2-p001 | silent prediction off | Runtime Type Annotation Evaluation with Forward Ref | type annotations evaluated at runtime, typing-only imports |
| r2-p002 | fired, no fix | Optional Generic Proxy with Overloaded Equality | When used with Optional types in generics, 'x == None' might rely on overloaded equality in proxies, which 'is' bypasses. |
| r2-p003 | invalid | Local Shadowing of None Breaks Rewrite | If None is shadowed locally (e.g., 'None = 5'), the fix incorrectly rewrites to 'is 5' or 'is not 5', causing NameError or wrong semantics. |
| r2-p004 | fired, no fix | Evaluation Order with Side Effects in Arguments | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p005 | fired, no fix | Dict with Custom __eq__ and Hash Collision | dict ordering, duplicate keys, __hash__/__eq__ overrides, sets |
| r2-p006 | fired, no fix | Overridden __eq__ Changes Semantics | If __eq__ is overridden, 'x == None' may differ from 'x is None', changing truth value. |
| r2-p007 | fired, no fix | Class Attribute vs Descriptor with Side Effects | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p008 | invalid | Eval Context After Local None Rebinding | In eval() or exec() contexts, the rewritten string may fail if the local scope doesn't contain the built-in None after rebinding. |
Sources
r1-p001 · Mutable Default Argument Aliasing After Rewrite
1def f(a=[]):
2 if a == None:
3 a = [1]
4 a.append(2)
5 print(a)
6f()
7f()
8f()
[2] [2, 2] [2, 2, 2]
r1-p002 · Function Expecting Boolean from Equality Comparison
1def expects_bool(b):
2 return 'yes' if b else 'no'
3x = None
4result = expects_bool(x == None)
5print(result)
yes
r1-p003 · Bytes vs Str Encoding and Newline Handling
1data = b'hello\n'
2if data == None:
3 data = b''
4print(len(data))
5print(repr(data))
6 b'hello\n'
r1-p004 · Optional Types and Overloaded Equality in Proxies
1class OptionalInt:
2 def __init__(self, value):
3 self.value = value
4 def __eq__(self, other):
5 print('eq called')
6 return self.value is None and other is None
7x = OptionalInt(None)
8print(x == None)
eq called True
r1-p005 · Comments and Docstrings Attached to Removed Code
1x = None
2# Check if uninitialized before use
3if x == None: # safety guard for hot path
4 x = 42
5print(x)
42
r1-p006 · Star-Imports, Module-Level Side Effects, __all__
1__all__ = ['config']
2config = None
3if config == None:
4 config = {'mode': 'prod'}
5print(config)
{'mode': 'prod'}r1-p007 · Generators vs Lists: Iterators Consumed Once (generated program does not parse: Traceback (most recent call last):)
1def gen():
2 for i in [None, 1]:
3 yield i
4 g = gen()
5print(list(x == None for x in g))
r1-p008 · Class Attributes vs Instance Attributes, Descriptors, Properties
1class Validator:
2 def __get__(self, obj, objtype):
3 print('get called')
4 return getattr(obj, '_x', None)
5class MyClass:
6 x = Validator()
7 def __init__(self):
8 self._x = None
9obj = MyClass()
10print(obj.x == None)
get called True
r2-p001 · Runtime Type Annotation Evaluation with Forward Ref
1import typing
2from typing import get_type_hints
3T = typing.TypeVar('T')
4def func(x: 'Optional[T]') -> None:
5 pass
6try:
7 hints = get_type_hints(func)
8 x = hints['x']
9 # This annotation is evaluated at runtime via get_type_hints
10 # It contains a string forward-ref that resolves to Optional[T]
11 # Now test equality with None inside a context that depends on the resolved type
12 val = None
13 if x == type(val): # Rule triggers: compares type(None) == type(None) via ==
14 print('equal')
15 else:
16 print('not equal')
17except Exception as e:
18 print(f'{type(e).__name__}: {e}')
NameError: name 'Optional' is not defined
r2-p002 · Optional Generic Proxy with Overloaded Equality
1class Proxy:
2 def __init__(self, value):
3 self.value = value
4 def __eq__(self, other):
5 if other is None:
6 return self.value is None
7 return self.value == other
8 def __is__(self, other): # Not real, but we simulate intent
9 pass
10# Simulate a generic Optional-like wrapper
11Maybe = Proxy
12x = Maybe(None)
13if x == None: # Rule triggers: uses __eq__, returns True because value is None
14 print('eq_true')
15else:
16 print('eq_false')
17# After fix: x is None -> False, because x is a Proxy instance, not None
18if x is None:
19 print('is_true')
20else:
21 print('is_false')
eq_true is_false
r2-p003 · Local Shadowing of None Breaks Rewrite (generated program does not parse: Traceback (most recent call last):)
1def test():
2 None = 5 # Shadow built-in None
3 x = 5
4 try:
5 if x == None: # Rule triggers: compares x == 5 -> True
6 print('eq_true')
7 else:
8 print('eq_false')
9 except Exception as e:
10 print(f'{type(e).__name__}: {e}')
11 # After broken fix: becomes 'x is 5' -> True (same value, but still wrong intent)
12 # But we show the original behavior is preserved in this version
13 # The hazard is that the fix would break meaning — we show what the fix would do
14 # by simulating the incorrect rewrite
15 try:
16 if x is None: # This is what the buggy fix would produce
17 print('is_true')
18 else:
19 print('is_false')
20 except Exception as e:
21 print(f'{type(e).__name__}: {e}')
22test()
r2-p004 · Evaluation Order with Side Effects in Arguments
1counter = 0
2
3def side_effect():
4 global counter
5 counter += 1
6 return None
7
8x = side_effect() # counter=1, x=None
9if x == None: # Rule triggers: x is None from side_effect, but we use ==
10 print(f'eq_true, counter={counter}')
11else:
12 print(f'eq_false, counter={counter}')
13
14# Reset and test again to show determinism
15counter = 0
16x = side_effect()
17if x is None: # What the fix would produce
18 print(f'is_true, counter={counter}')
19else:
20 print(f'is_false, counter={counter}')
eq_true, counter=1 is_true, counter=1
r2-p005 · Dict with Custom __eq__ and Hash Collision
1class EqNone:
2 def __eq__(self, other):
3 return other is None or (isinstance(other, EqNone) and self.value == other.value)
4 def __hash__(self):
5 return 42 # Fixed hash to cause collision
6 def __init__(self, value):
7 self.value = value
8
9# Create two distinct EqNone instances that hash to same bucket
10a = EqNone(1)
11b = EqNone(2)
12d = {a: 'first', b: 'second'} # Due to hash collision, second overwrites first? Actually, dict uses eq too
13# But we care about == None behavior
14val = EqNone(99)
15if val == None: # Rule triggers: returns False because val.value=99 != None
16 print('eq_true')
17else:
18 print('eq_false')
19# After fix: val is None -> False (same result here, but we show the path)
20# To make it differ, change __eq__ to return True for None
21class EqNoneTrue:
22 def __eq__(self, other):
23 return other is None
24 def __hash__(self):
25 return 0
26v = EqNoneTrue()
27if v == None: # True due to __eq__
28 print('eq_true_overridden')
29else:
30 print('eq_false_overridden')
31if v is None: # Fix would make this: False
32 print('is_true_overridden')
33else:
34 print('is_false_overridden')
eq_true eq_true_overridden is_false_overridden
r2-p006 · Overridden __eq__ Changes Semantics
1class FakeNone:
2 def __eq__(self, other):
3 return True # Pretends to equal everything, including None
4 def __ne__(self, other):
5 return False
6
7x = FakeNone()
8print(f'x == None: {x == None}') # Rule triggers: True due to __eq__
9print(f'x is None: {x is None}') # False: x is instance, not None
10# After fix: 'x is None' would be False — but originally 'x == None' was True
11# So behavior flips: True -> False
12print(f'After fix simulation: {x is None}')
x == None: True x is None: False After fix simulation: False
r2-p007 · Class Attribute vs Descriptor with Side Effects
1class Descriptor:
2 def __get__(self, obj, objtype):
3 print('Descriptor accessed')
4 return None
5 def __set__(self, obj, value):
6 pass
7
8class MyClass:
9 attr = Descriptor()
10 def __init__(self):
11 self.instance_attr = None
12
13obj = MyClass()
14# Trigger rule on class attribute access via instance
15if obj.attr == None: # Rule triggers: calls __get__ -> prints, returns None -> None == None -> True
16 print('class_attr_eq_true')
17else:
18 print('class_attr_eq_false')
19# After fix: obj.attr is None -> still True, but __get__ called once (same)
20# Now make it differ: change descriptor to return something equal to None but not None
21class FakeNoneDescriptor:
22 def __get__(self, obj, objtype):
23 print('Descriptor accessed')
24 return FakeNone() # From previous class
25
26class MyClass2:
27 attr = FakeNoneDescriptor()
28
29obj2 = MyClass2()
30if obj2.attr == None: # __eq__ of FakeNone returns True -> True
31 print('fake_desc_eq_true')
32else:
33 print('fake_desc_eq_false')
34if obj2.attr is None: # Fix: returns False
35 print('fake_desc_is_true')
36else:
37 print('fake_desc_is_false')
Descriptor accessed class_attr_eq_true Descriptor accessed
r2-p008 · Eval Context After Local None Rebinding (generated program does not parse: Traceback (most recent call last):)
1def test_eval():
2 None = 42 # Shadow built-in None
3 x = None # x is 42
4 # Original code: x == None -> 42 == 42 -> True
5 try:
6 result = eval('x == None') # Uses local None=42 -> True
7 print(f'eval_result: {result}')
8 except Exception as e:
9 print(f'{type(e).__name__}: {e}')
10 # After buggy fix: becomes 'x is None' -> but None is now 42 -> 'x is 42' -> True (same)
11 # To make it fail, we need the fix to refer to built-in None, but it's shadowed
12 # So simulate: what if the fix incorrectly used the built-in None name without checking scope?
13 # But Ruff's fix is local — it rewrites the AST, so 'is None' still refers to local None
14 # So we show: if the fix were to somehow use the built-in None (which it doesn't), it would break
15 # Instead, we show the hazard: if None is rebound, the fix still works locally — but the *intent* is broken
16 # We instead show: in eval, if the string is rewritten and executed in a scope where None is not defined...
17 del None # Remove local None
18 try:
19 # This would be the broken fix: if Ruff rewrote to 'x is None' but None is gone
20 result = eval('x is None') # NameError: name 'None' is not defined
21 print(f'eval_is_result: {result}')
22 except Exception as e:
23 print(f'{type(e).__name__}: {e}')
24
25test_eval()