ruff FURB169
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Side effect duplication from function call | 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. |
| r1-p002 | invalid | Comment loss during AST rewrite | When 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-p003 | fix ok | Invalid syntax in type annotation | 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. |
| r1-p004 | fix ok prediction off | Typed-only imports evaluated at runtime | type annotations evaluated at runtime, typing-only imports |
| r1-p005 | fix ok prediction off | Bytes vs str encoding and newline handling | bytes vs str, encoding and newline handling, text vs binary modes |
| r1-p006 | fix ok | Larger expression precedence breakage | 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. |
| r1-p007 | fix ok | Class vs instance attributes with descriptors | class attributes vs instance attributes, descriptors, properties with side effects |
| r1-p008 | fix ok | Evaluation order and side effects in merged expressions | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p001 | fix ok | Type annotations evaluated at runtime with typing-only import | type annotations evaluated at runtime, typing-only imports |
| r2-p002 | fix ok | Custom __bool__ affecting truthiness vs identity | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r2-p003 | fix ok | Context manager with write but no close | context managers that are not files, objects with write() but no close() |
| r2-p004 | fix ok | Generator consumed once in conditional | generators vs lists, iterators consumed once, lazy vs eager evaluation |
| r2-p005 | fix ok | Comment attached to type expression lost during rewrite | comments and docstrings attached to the removed or moved code |
| r2-p006 | fix ok | Mutable default argument capturing rewritten expression | mutable default arguments and aliasing after a rewrite |
| r2-p007 | fix ok | Bytes vs str in binary mode file handling | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p008 | fix ok | String formatting with % and .format in condition | string 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