fixhunt

ruff FURB168

labelled unknown
Runs / 2026-10-17T21-35-36-511Z-gb35-ruff-FURB168ruff 0.17.12026-10-17 21:35 UTC8 programs × 2 round(s)16 programs, 15 fired, 15 fixed, 0 findings, 0 rejectedmodel cost $0.0042

Analysis Nemotron 3 Super

Flags isinstance checks against type(None), recommending replacement with identity comparison.

Fix: Replaces isinstance(expr, type(None)) with expr is None, preserving the original expression and surrounding context exactly.

8 hazards
  • Rewriting isinstance(obj, type(None)) when obj has a __class__ attribute that raises during access changes timing of AttributeError.
  • If obj is a property with side effects in its getter, evaluating it once for isinstance vs once for is may differ if the property is not idempotent.
  • In code like isinstance(getattr(x, 'y', type(None)), type(None)), the fix alters which expression is identity-compared, potentially changing None vs type(None) confusion.
  • When isinstance appears in a lambda default argument, moving the expression may alter closure capture timing.
  • If the isinstance call is inside a statement with a trailing comment, the fix may drop the comment if not carefully reapplied.
  • In isinstance(obj, type(None)) where obj is None, both versions are True, but if obj is a custom object with __eq__ overriding equality to None, the semantic intent differs (though rule targets only type(None) checks).
  • In isinstance(1/0, type(None)), the ZeroDivisionError occurs during isinstance evaluation; after fix, it occurs during 'is' evaluation—same timing, but if wrapped in a context that catches exceptions differently due to stack depth, rare but possible.
  • If type(None) is rebound in local scope (e.g., type = lambda: None), isinstance(x, type(None)) uses the local binding, but fix uses global None—this is a hazard only if type is shadowed, which the rule assumes is not the case per documentation.

Programs

idresulttitlehazard
r1-p001fix okBytes vs Str Encoding Newlinebytes vs str, encoding and newline handling, text vs binary modes
r1-p002fix okComments and Docstrings Attachedcomments and docstrings attached to the removed or moved code
r1-p003fix okContext Managers Not Filescontext managers that are not files, objects with write() but no close()
r1-p004fix okZeroDivisionError TimingIn isinstance(1/0, type(None)), the ZeroDivisionError occurs during isinstance evaluation; after fix, it occurs during 'is' evaluation—same timing, but if wrapped in a context that catches exceptions
r1-p005fix okTrailing Comment DroppedIf the isinstance call is inside a statement with a trailing comment, the fix may drop the comment if not carefully reapplied.
r1-p006silent prediction offType Shadowed LocallyIf type(None) is rebound in local scope (e.g., type = lambda: None), isinstance(x, type(None)) uses the local binding, but fix uses global None—this is a hazard only if type is shadowed, which the rul
r1-p007fix ok prediction offCustom Object __eq__ OverrideIn isinstance(obj, type(None)) where obj is None, both versions are True, but if obj is a custom object with __eq__ overriding equality to None, the semantic intent differs (though rule targets only t
r1-p008fix okType Annotations Runtimetype annotations evaluated at runtime, typing-only imports
r2-p001fix okCustom Equality OverrideIn isinstance(obj, type(None)) where obj is None, both versions are True, but if obj is a custom object with __eq__ overriding equality to None, the semantic intent differs.
r2-p002fix okGenerator Exhaustiongenerators vs lists, iterators consumed once, lazy vs eager evaluation
r2-p003fix okTruthiness with Custom __bool__truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r2-p004fix okKeyword-Only Argument Defaultskeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p005fix okContext Manager with Write But No Closecontext managers that are not files, objects with write() but no close()
r2-p006fix okClass Attribute Descriptor Side Effectclass attributes vs instance attributes, descriptors, properties with side effects
r2-p007fix okEvaluation Order with Side Effectsevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p008fix okDict Ordering and Hash Equalsdict ordering, duplicate keys, __hash__/__eq__ overrides, sets

Sources

r1-p001 · Bytes vs Str Encoding Newline
1data = b'hello\nworld\r\n'
2txt = data.decode().replace('\r\n', '\n')
3if isinstance(txt, type(None)):
4 print('none')
5else:
6 with open('test.txt', 'w', encoding='utf-8') as f:
7 f.write(txt)
8with open('test.txt', 'r', encoding='utf-8') as f:
9 print(repr(f.read()))
10import os; os.remove('test.txt')
--- orig.py	2026-10-17 17:35:49.137708540 -0400
+++ fixed.py	2026-10-17 17:35:49.301706761 -0400
@@ -1,6 +1,6 @@
 data = b'hello\nworld\r\n'
 txt = data.decode().replace('\r\n', '\n')
-if isinstance(txt, type(None)):
+if txt is None:
     print('none')
 else:
     with open('test.txt', 'w', encoding='utf-8') as f:
'hello\nworld\n'
r1-p002 · Comments and Docstrings Attached
1def foo():
2 """This function checks for None."""
3 x = None # input value
4 if isinstance(x, type(None)): # is it None? yes
5 return 'null'
6 return 'not null'
7print(foo())
--- orig.py	2026-10-17 17:35:49.408705601 -0400
+++ fixed.py	2026-10-17 17:35:49.562703931 -0400
@@ -1,7 +1,7 @@
 def foo():
     """This function checks for None."""
     x = None  # input value
-    if isinstance(x, type(None)):  # is it None? yes
+    if x is None:  # is it None? yes
         return 'null'
     return 'not null'
 print(foo())
null
r1-p003 · Context Managers Not Files
1class Logger:
2 def __enter__(self):
3 self.buffer = []
4 return self
5 def __exit__(self, *exc):
6 pass
7 def write(self, s):
8 self.buffer.append(s)
9with Logger() as log:
10 if isinstance(log, type(None)):
11 log.write('is none')
12 else:
13 log.write('is logger')
14print(''.join(log.buffer))
--- orig.py	2026-10-17 17:35:49.667702792 -0400
+++ fixed.py	2026-10-17 17:35:49.825701079 -0400
@@ -7,7 +7,7 @@
     def write(self, s):
         self.buffer.append(s)
 with Logger() as log:
-    if isinstance(log, type(None)):
+    if log is None:
         log.write('is none')
     else:
         log.write('is logger')
is logger
r1-p004 · ZeroDivisionError Timing
1try:
2 if isinstance(1/0, type(None)):
3 print('unreachable')
4 else:
5 print('not none')
6except ZeroDivisionError as e:
7 print(f'{type(e).__name__}: {e}')
--- orig.py	2026-10-17 17:35:49.927699972 -0400
+++ fixed.py	2026-10-17 17:35:50.094698163 -0400
@@ -1,5 +1,5 @@
 try:
-    if isinstance(1/0, type(None)):
+    if 1 / 0 is None:
         print('unreachable')
     else:
         print('not none')
ZeroDivisionError: division by zero
r1-p005 · Trailing Comment Dropped
1x = None
2if isinstance(x, type(None)): # check if null
3 print('yes')
4else:
5 print('no')
--- orig.py	2026-10-17 17:35:50.197697047 -0400
+++ fixed.py	2026-10-17 17:35:50.372695151 -0400
@@ -1,5 +1,5 @@
 x = None
-if isinstance(x, type(None)):  # check if null
+if x is None:  # check if null
     print('yes')
 else:
     print('no')
yes
r1-p006 · Type Shadowed Locally
1def test():
2 type = lambda: None # shadow built-in
3 x = None
4 if isinstance(x, type(None)): # uses local type -> type(None) -> None
5 return 'local'
6 return 'global'
7print(test())
r1-p007 · Custom Object __eq__ Override
1class FakeNone:
2 def __eq__(self, other):
3 return None
4obj = FakeNone()
5print(isinstance(obj, type(None))) # False
6print(obj is None) # False
7print(obj == None) # None
--- orig.py	2026-10-17 17:35:50.820690299 -0400
+++ fixed.py	2026-10-17 17:35:50.978688588 -0400
@@ -2,6 +2,6 @@
     def __eq__(self, other):
         return None
 obj = FakeNone()
-print(isinstance(obj, type(None)))  # False
+print((obj is None))  # False
 print(obj is None)                # False
 print(obj == None)                # None
False
False
None
r1-p008 · Type Annotations Runtime
1def func(x: int) -> None:
2 pass
3if isinstance(func.__annotations__['return'], type(None)):
4 print('returns none')
5else:
6 print('returns something else')
--- orig.py	2026-10-17 17:35:51.085687429 -0400
+++ fixed.py	2026-10-17 17:35:51.244685709 -0400
@@ -1,6 +1,6 @@
 def func(x: int) -> None:
     pass
-if isinstance(func.__annotations__['return'], type(None)):
+if func.__annotations__['return'] is None:
     print('returns none')
 else:
     print('returns something else')
returns none
r2-p001 · Custom Equality Override
1class X:
2 def __eq__(self, other):
3 return other is None
4x = X()
5print(isinstance(x, type(None))) # Should become x is None
6print('Result:', isinstance(x, type(None)))
--- orig.py	2026-10-17 17:35:56.918624505 -0400
+++ fixed.py	2026-10-17 17:35:57.088622677 -0400
@@ -2,5 +2,5 @@
     def __eq__(self, other):
         return other is None
 x = X()
-print(isinstance(x, type(None)))  # Should become x is None
-print('Result:', isinstance(x, type(None)))
+print((x is None))  # Should become x is None
+print('Result:', (x is None))
False
Result: False
r2-p002 · Generator Exhaustion
1def gen():
2 yield 1
3g = gen()
4isinstance(next(g), type(None)) # First use consumes
5try:
6 isinstance(next(g), type(None))
7except StopIteration as e:
8 print('StopIteration caught')
9else:
10 print('No exception')
--- orig.py	2026-10-17 17:35:57.189621593 -0400
+++ fixed.py	2026-10-17 17:35:57.344619927 -0400
@@ -1,9 +1,9 @@
 def gen():
     yield 1
 g = gen()
-isinstance(next(g), type(None))  # First use consumes
+next(g) is None  # First use consumes
 try:
-    isinstance(next(g), type(None))
+    next(g) is None
 except StopIteration as e:
     print('StopIteration caught')
 else:
StopIteration caught
r2-p003 · Truthiness with Custom __bool__
1class Y:
2 def __bool__(self):
3 return False
4y = Y()
5before = isinstance(y, type(None))
6after = y is None
7print(f'isinstance: {before}, is None: {after}')
8if isinstance(y, type(None)):
9 print('Taken')
10else:
11 print('Not taken')
--- orig.py	2026-10-17 17:35:57.443618863 -0400
+++ fixed.py	2026-10-17 17:35:57.603617145 -0400
@@ -2,10 +2,10 @@
     def __bool__(self):
         return False
 y = Y()
-before = isinstance(y, type(None))
+before = y is None
 after = y is None
 print(f'isinstance: {before}, is None: {after}')
-if isinstance(y, type(None)):
+if y is None:
     print('Taken')
 else:
     print('Not taken')
isinstance: False, is None: False
Not taken
r2-p004 · Keyword-Only Argument Defaults
1def f(*, x=[]):
2 isinstance(x.pop() if x else None, type(None))
3 return len(x)
4print(f()) # First call
5print(f()) # Second call, x is same list
--- orig.py	2026-10-17 17:35:57.706616038 -0400
+++ fixed.py	2026-10-17 17:35:57.864614341 -0400
@@ -1,5 +1,5 @@
 def f(*, x=[]):
-    isinstance(x.pop() if x else None, type(None))
+    (x.pop() if x else None) is None
     return len(x)
 print(f())  # First call
 print(f())  # Second call, x is same list
0
0
r2-p005 · Context Manager with Write But No Close
1class SilentWriter:
2 def write(self, msg):
3 pass
4 def __enter__(self):
5 return self
6 def __exit__(self, *exc):
7 pass
8with SilentWriter() as w:
9 w.write('test')
10 result = isinstance(w, type(None))
11 print('isinstance result:', result)
--- orig.py	2026-10-17 17:35:57.962613288 -0400
+++ fixed.py	2026-10-17 17:35:58.125611538 -0400
@@ -7,5 +7,5 @@
         pass
 with SilentWriter() as w:
     w.write('test')
-    result = isinstance(w, type(None))
+    result = w is None
     print('isinstance result:', result)
isinstance result: False
r2-p006 · Class Attribute Descriptor Side Effect
1class Desc:
2 def __get__(self, obj, owner):
3 print('Descriptor accessed')
4 return None
5class A:
6 attr = Desc()
7a = A()
8print(isinstance(a.attr, type(None))) # Triggers descriptor
9print(isinstance(a.attr, type(None))) # Again
--- orig.py	2026-10-17 17:35:58.231610401 -0400
+++ fixed.py	2026-10-17 17:35:58.397608619 -0400
@@ -5,5 +5,5 @@
 class A:
     attr = Desc()
 a = A()
-print(isinstance(a.attr, type(None)))  # Triggers descriptor
-print(isinstance(a.attr, type(None)))  # Again
+print((a.attr is None))  # Triggers descriptor
+print((a.attr is None))  # Again
Descriptor accessed
True
Descriptor accessed
True
r2-p007 · Evaluation Order with Side Effects
1counter = 0
2def inc():
3 global counter
4 counter += 1
5 return None
6print(counter)
7isinstance(inc(), type(None))
8print('After:', counter)
9counter = 0
10print(counter)
11result = inc() is None
12print('After is None:', counter)
--- orig.py	2026-10-17 17:35:58.497607546 -0400
+++ fixed.py	2026-10-17 17:35:58.660605797 -0400
@@ -4,7 +4,7 @@
     counter += 1
     return None
 print(counter)
-isinstance(inc(), type(None))
+inc() is None
 print('After:', counter)
 counter = 0
 print(counter)
0
After: 1
0
After is None: 1
r2-p008 · Dict Ordering and Hash Equals
1class Z:
2 def __init__(self, val):
3 self.val = val
4 def __hash__(self):
5 return hash(self.val)
6 def __eq__(self, other):
7 return self.val == other.val
8d = {}
9z1 = Z(1)
10z2 = Z(1)
11d[z1] = 'first'
12d[z2] = 'second' # Same hash, equal -> overwrites
13for k in d:
14 print(isinstance(d[k], type(None)), end=' ') # Should be False twice
15print()
--- orig.py	2026-10-17 17:35:58.756604767 -0400
+++ fixed.py	2026-10-17 17:35:58.927602931 -0400
@@ -11,5 +11,5 @@
 d[z1] = 'first'
 d[z2] = 'second'  # Same hash, equal -> overwrites
 for k in d:
-    print(isinstance(d[k], type(None)), end=' ')  # Should be False twice
+    print((d[k] is None), end=' ')  # Should be False twice
 print()
False