ruff FURB168
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Bytes vs Str Encoding Newline | bytes vs str, encoding and newline handling, text vs binary modes |
| r1-p002 | fix ok | Comments and Docstrings Attached | comments and docstrings attached to the removed or moved code |
| r1-p003 | fix ok | Context Managers Not Files | context managers that are not files, objects with write() but no close() |
| r1-p004 | fix ok | ZeroDivisionError Timing | 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 |
| r1-p005 | fix ok | Trailing Comment Dropped | If the isinstance call is inside a statement with a trailing comment, the fix may drop the comment if not carefully reapplied. |
| r1-p006 | silent prediction off | Type Shadowed Locally | 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 rul |
| r1-p007 | fix ok prediction off | Custom Object __eq__ Override | 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 t |
| r1-p008 | fix ok | Type Annotations Runtime | type annotations evaluated at runtime, typing-only imports |
| r2-p001 | fix ok | Custom Equality Override | 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. |
| r2-p002 | fix ok | Generator Exhaustion | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r2-p003 | fix ok | Truthiness with Custom __bool__ | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r2-p004 | fix ok | Keyword-Only Argument Defaults | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p005 | fix ok | Context Manager with Write But No Close | context managers that are not files, objects with write() but no close() |
| r2-p006 | fix ok | Class Attribute Descriptor Side Effect | class attributes vs instance attributes, descriptors, properties with side effects |
| r2-p007 | fix ok | Evaluation Order with Side Effects | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p008 | fix ok | Dict Ordering and Hash Equals | dict 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