fixhunt

ruff FURB169

labelled unknown
Runs / 2026-10-17T21-35-59-086Z-clol-ruff-FURB169ruff 0.17.12026-10-17 21:35 UTC8 programs × 2 round(s)16 programs, 15 fired, 15 fixed, 0 findings, 1 rejectedmodel cost $0.0032

Analysis Nemotron 3 Super

The rule flags direct comparison of an object's type to the type of None using 'type(x) is type(None)' patterns.

Fix: Replace 'type(X) is type(None)' with 'X is None', 'type(X) == type(None)' with 'X is None', 'type(X) is not type(None)' with 'X is not None', and 'type(X) != type(None)' with 'X is not None'.

10 hazards
  • Rewriting 'type(obj) is type(None)' when 'obj' is a property with side effects changes evaluation timing and may execute getter twice.
  • If 'obj' is a function call like 'type(func()) is type(None)', the fix 'func() is None' alters side effects by calling func() once instead of evaluating its return type.
  • In default arguments like 'def f(x=type(obj) is type(None)):', moving the expression changes when 'obj' is evaluated relative to function definition.
  • When used in annotations like 'List[type(obj) is type(None)]', the fix breaks syntax as 'obj is None' is not a valid type annotation.
  • If 'obj' is a class attribute accessed via 'type(cls.attr) is type(None)', the fix may trigger __getattribute__ differently under metaclasses.
  • In comprehensions with filtering like '[x for x in ys if type(x) is type(None)]', the fix preserves behavior but may affect performance characteristics in tight loops.
  • When 'obj' is a variable named 'None' in local scope, 'type(None) is type(None)' becomes ambiguous after fix to 'None is None' due to shadowing.
  • If the comparison is part of a larger expression like 'type(a) is type(None) and b', fixing only part may create invalid syntax or alter precedence.
  • In try/except blocks where 'obj' evaluation raises exceptions, the fix changes whether the exception occurs during type checking or identity check.
  • When comments are attached to the 'type' expression like 'type(x) # check is type(None)', the fix may detach or lose the comment during AST rewrite.

Programs

idresulttitlehazard
r1-p001fix okSide effect duplication from function callIf 'obj' is a function call like 'type(func()) is type(None)', the fix 'func() is None' alters side effects by calling func() once instead of evaluating its return type.
r1-p002invalidComment loss during AST rewriteWhen comments are attached to the 'type' expression like 'type(x) # check\nis type(None)', the fix may detach or lose the comment during AST rewrite.
r1-p003fix okInvalid syntax in type annotationWhen used in annotations like 'List[type(obj) is type(None)]', the fix breaks syntax as 'obj is None' is not a valid type annotation.
r1-p004fix ok prediction offTyped-only imports evaluated at runtimetype annotations evaluated at runtime, typing-only imports
r1-p005fix ok prediction offBytes vs str encoding and newline handlingbytes vs str, encoding and newline handling, text vs binary modes
r1-p006fix okLarger expression precedence breakageIf the comparison is part of a larger expression like 'type(a) is type(None) and b', fixing only part may create invalid syntax or alter precedence.
r1-p007fix okClass vs instance attributes with descriptorsclass attributes vs instance attributes, descriptors, properties with side effects
r1-p008fix okEvaluation order and side effects in merged expressionsevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p001fix okType annotations evaluated at runtime with typing-only importtype annotations evaluated at runtime, typing-only imports
r2-p002fix okCustom __bool__ affecting truthiness vs identitytruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r2-p003fix okContext manager with write but no closecontext managers that are not files, objects with write() but no close()
r2-p004fix okGenerator consumed once in conditionalgenerators vs lists, iterators consumed once, lazy vs eager evaluation
r2-p005fix okComment attached to type expression lost during rewritecomments and docstrings attached to the removed or moved code
r2-p006fix okMutable default argument capturing rewritten expressionmutable default arguments and aliasing after a rewrite
r2-p007fix okBytes vs str in binary mode file handlingbytes vs str, encoding and newline handling, text vs binary modes
r2-p008fix okString formatting with % and .format in conditionstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes

Sources

r1-p001 · Side effect duplication from function call
1counter = 0
2
3def make_obj():
4 global counter
5 counter += 1
6 return None
7
8if type(make_obj()) is type(None):
9 print('before:', counter)
10else:
11 print('unreachable')
12print('after:', counter)
--- orig.py	2026-10-17 17:36:07.671509615 -0400
+++ fixed.py	2026-10-17 17:36:07.835507873 -0400
@@ -5,7 +5,7 @@
     counter += 1
     return None
 
-if type(make_obj()) is type(None):
+if make_obj() is None:
     print('before:', counter)
 else:
     print('unreachable')
before: 1
after: 1
r1-p002 · Comment loss during AST rewrite (generated program does not parse: Traceback (most recent call last):)
1x = None
2# This line has a comment after type(
3if type(x) # check
4is type(None):
5 print('matched')
6else:
7 print('not matched')
r1-p003 · Invalid syntax in type annotation
1from typing import List
2
3flag = type(lambda: None) is type(None)
4arr: List[flag] = [] # This line will trigger FURB169 on the annotation
5print('arr type:', type(arr))
6print('flag value:', flag)
--- orig.py	2026-10-17 17:36:08.089505176 -0400
+++ fixed.py	2026-10-17 17:36:08.270503256 -0400
@@ -1,6 +1,6 @@
 from typing import List
 
-flag = type(lambda: None) is type(None)
+flag = (lambda: None) is None
 arr: List[flag] = []  # This line will trigger FURB169 on the annotation
 print('arr type:', type(arr))
 print('flag value:', flag)
arr type: <class 'list'>
flag value: False
r1-p004 · Typed-only imports evaluated at runtime
1from typing import TYPE_CHECKING
2if TYPE_CHECKING:
3 from nonexistent import something # This import is only for type checking
4
5x = None
6if type(x) is type(None):
7 print('x is None')
8else:
9 print('x is not None')
--- orig.py	2026-10-17 17:36:08.374502153 -0400
+++ fixed.py	2026-10-17 17:36:08.556500222 -0400
@@ -3,7 +3,7 @@
     from nonexistent import something  # This import is only for type checking
 
 x = None
-if type(x) is type(None):
+if x is None:
     print('x is None')
 else:
     print('x is not None')
x is None
r1-p005 · Bytes vs str encoding and newline handling
1data_b = b'hello\nworld'
2data_s = 'hello\nworld'
3print('bytes lines:', len(data_b.splitlines()))
4print('str lines:', len(data_s.splitlines()))
5if type(data_b) is type(None):
6 print('bytes is None')
7else:
8 print('bytes is not None')
--- orig.py	2026-10-17 17:36:08.668499034 -0400
+++ fixed.py	2026-10-17 17:36:08.830497315 -0400
@@ -2,7 +2,7 @@
 data_s = 'hello\nworld'
 print('bytes lines:', len(data_b.splitlines()))
 print('str lines:', len(data_s.splitlines()))
-if type(data_b) is type(None):
+if data_b is None:
     print('bytes is None')
 else:
     print('bytes is not None')
bytes lines: 2
str lines: 2
bytes is not None
r1-p006 · Larger expression precedence breakage
1a = None
2b = False
3result = type(a) is type(None) and b
4print('Result of type(a) is type(None) and b:', result)
5# After fix: a is None and b -> evaluates to False
6# But we want to see if the fix changes grouping
7result2 = (type(a) is type(None)) and b
8print('With explicit parens:', result2)
--- orig.py	2026-10-17 17:36:08.937496180 -0400
+++ fixed.py	2026-10-17 17:36:09.101494442 -0400
@@ -1,8 +1,8 @@
 a = None
 b = False
-result = type(a) is type(None) and b
+result = (a is None) and b
 print('Result of type(a) is type(None) and b:', result)
 # After fix: a is None and b -> evaluates to False
 # But we want to see if the fix changes grouping
-result2 = (type(a) is type(None)) and b
+result2 = ((a is None)) and b
 print('With explicit parens:', result2)
Result of type(a) is type(None) and b: False
With explicit parens: False
r1-p007 · Class vs instance attributes with descriptors
1class MyClass:
2 def __init__(self):
3 self._val = None
4
5 @property
6 def val(self):
7 print('Property accessed')
8 return self._val
9
10obj = MyClass()
11if type(obj.val) is type(None):
12 print('obj.val is None')
13else:
14 print('obj.val is not None')
--- orig.py	2026-10-17 17:36:09.208493308 -0400
+++ fixed.py	2026-10-17 17:36:09.368491612 -0400
@@ -8,7 +8,7 @@
         return self._val
 
 obj = MyClass()
-if type(obj.val) is type(None):
+if obj.val is None:
     print('obj.val is None')
 else:
     print('obj.val is not None')
Property accessed
obj.val is None
r1-p008 · Evaluation order and side effects in merged expressions
1side_effects = []
2
3def record(x):
4 side_effects.append(x)
5 return x
6
7a = record(None)
8b = record(None)
9# This condition uses type(a) is type(None) in a compound expression
10if type(a) is type(None) and type(b) is type(None):
11 print('Both are None')
12else:
13 print('At least one is not None')
14print('Side effects recorded:', side_effects)
15# After fix: a is None and b is None
16# Should still record two effects, but we're testing if the rule duplicates/deletes
--- orig.py	2026-10-17 17:36:09.469490542 -0400
+++ fixed.py	2026-10-17 17:36:09.626488879 -0400
@@ -7,7 +7,7 @@
 a = record(None)
 b = record(None)
 # This condition uses type(a) is type(None) in a compound expression
-if type(a) is type(None) and type(b) is type(None):
+if (a is None) and (b is None):
     print('Both are None')
 else:
     print('At least one is not None')
Both are None
Side effects recorded: [None, None]
r2-p001 · Type annotations evaluated at runtime with typing-only import
1from typing import Optional
2
3def func(x: Optional[int]) -> bool:
4 return type(x) is type(None)
5
6print('Before:', func(5))
7print('After :', func(None))
--- orig.py	2026-10-17 17:36:13.815444603 -0400
+++ fixed.py	2026-10-17 17:36:14.000442652 -0400
@@ -1,7 +1,7 @@
 from typing import Optional
 
 def func(x: Optional[int]) -> bool:
-    return type(x) is type(None)
+    return x is None
 
 print('Before:', func(5))
 print('After :', func(None))
Before: False
After : True
r2-p002 · Custom __bool__ affecting truthiness vs identity
1class FakeNone:
2 def __bool__(self):
3 return False
4 def __eq__(self, other):
5 return other is None
6
7obj = FakeNone()
8print('obj is None:', obj is None)
9print('obj == None:', obj == None)
10print('type(obj) is type(None):', type(obj) is type(None))
--- orig.py	2026-10-17 17:36:14.106441535 -0400
+++ fixed.py	2026-10-17 17:36:14.271439797 -0400
@@ -7,4 +7,4 @@
 obj = FakeNone()
 print('obj is None:', obj is None)
 print('obj == None:', obj == None)
-print('type(obj) is type(None):', type(obj) is type(None))
+print('type(obj) is type(None):', (obj is None))
obj is None: False
obj == None: True
type(obj) is type(None): False
r2-p003 · Context manager with write but no close
1class Writer:
2 def __init__(self):
3 self.data = ''
4 def write(self, s):
5 self.data += s
6
7w = Writer()
8if type(w) is type(None):
9 w.write('unreachable')
10else:
11 w.write('reached')
12print(w.data)
--- orig.py	2026-10-17 17:36:14.368438775 -0400
+++ fixed.py	2026-10-17 17:36:14.527437100 -0400
@@ -5,7 +5,7 @@
         self.data += s
 
 w = Writer()
-if type(w) is type(None):
+if w is None:
     w.write('unreachable')
 else:
     w.write('reached')
reached
r2-p004 · Generator consumed once in conditional
1def gen():
2 yield 1
3 yield None
4
5g = gen()
6first = next(g)
7if type(first) is type(None):
8 print('first is None')
9else:
10 print('first is not None:', first)
11second = next(g)
12if type(second) is type(None):
13 print('second is None')
14else:
15 print('second is not None:', second)
--- orig.py	2026-10-17 17:36:14.633435983 -0400
+++ fixed.py	2026-10-17 17:36:14.795434276 -0400
@@ -4,12 +4,12 @@
 
 g = gen()
 first = next(g)
-if type(first) is type(None):
+if first is None:
     print('first is None')
 else:
     print('first is not None:', first)
 second = next(g)
-if type(second) is type(None):
+if second is None:
     print('second is None')
 else:
     print('second is not None:', second)
first is not None: 1
second is None
r2-p005 · Comment attached to type expression lost during rewrite
1x = 5
2if type(x) is type(None): # this comment should be preserved
3 print('unreachable')
4else:
5 print('x is not None:', x)
--- orig.py	2026-10-17 17:36:14.895433223 -0400
+++ fixed.py	2026-10-17 17:36:15.059431496 -0400
@@ -1,5 +1,5 @@
 x = 5
-if type(x) is type(None):  # this comment should be preserved
+if x is None:  # this comment should be preserved
     print('unreachable')
 else:
     print('x is not None:', x)
x is not None: 5
r2-p006 · Mutable default argument capturing rewritten expression
1def f(x=None, cache=[]):
2 if type(x) is type(None):
3 cache.append('none')
4 else:
5 cache.append(x)
6 return cache
7
8print('First call :', f())
9print('Second call:', f(1))
10print('Third call :', f())
--- orig.py	2026-10-17 17:36:15.157430464 -0400
+++ fixed.py	2026-10-17 17:36:15.318428770 -0400
@@ -1,5 +1,5 @@
 def f(x=None, cache=[]):
-    if type(x) is type(None):
+    if x is None:
         cache.append('none')
     else:
         cache.append(x)
First call : ['none']
Second call: ['none', 1]
Third call : ['none', 1, 'none']
r2-p007 · Bytes vs str in binary mode file handling
1with open('test.bin', 'wb') as f:
2 f.write(b'hello')
3
4with open('test.bin', 'rb') as f:
5 data = f.read()
6 if type(data) is type(None):
7 print('unreachable')
8 else:
9 print('read bytes:', data == b'hello')
10
11import os
12os.remove('test.bin')
--- orig.py	2026-10-17 17:36:15.417427728 -0400
+++ fixed.py	2026-10-17 17:36:15.580426013 -0400
@@ -3,7 +3,7 @@
 
 with open('test.bin', 'rb') as f:
     data = f.read()
-    if type(data) is type(None):
+    if data is None:
         print('unreachable')
     else:
         print('read bytes:', data == b'hello')
read bytes: True
r2-p008 · String formatting with % and .format in condition
1value = None
2result = 'value is %s' % (None if type(value) is type(None) else value)
3print('Formatted:', result)
4
5value = 42
6result = 'value is {}'.format(None if type(value) is type(None) else value)
7print('Formatted:', result)
--- orig.py	2026-10-17 17:36:15.682424939 -0400
+++ fixed.py	2026-10-17 17:36:15.846423214 -0400
@@ -1,7 +1,7 @@
 value = None
-result = 'value is %s' % (None if type(value) is type(None) else value)
+result = 'value is %s' % (None if (value is None) else value)
 print('Formatted:', result)
 
 value = 42
-result = 'value is {}'.format(None if type(value) is type(None) else value)
+result = 'value is {}'.format(None if (value is None) else value)
 print('Formatted:', result)
Formatted: value is None
Formatted: value is 42