fixhunt

ruff PLC1802

labelled unknown
Runs / 2026-10-17T20-57-48-497Z-mylx-ruff-PLC1802ruff 0.17.12026-10-17 20:57 UTC8 programs × 2 round(s)16 programs, 4 fired, 4 fixed, 0 findings, 0 rejectedmodel cost $0.0056

Analysis Nemotron 3 Super

Flags explicit len() comparisons where truthiness of a sequence alone suffices.

Fix: Replace 'len(seq)' with 'seq' directly, preserving any logical negation (!= becomes implicit falsy check, == 0 becomes 'not seq'), and drop the call while keeping surrounding operators/structure intact.

10 hazards
  • len() on a non-sequence type like int has no truthiness equivalent, causing TypeError if rewritten.
  • len() inside a lambda or comprehension may capture different variables if rewritten naively due to scope changes.
  • If len() appears in a macro-generated line via exec/eval, removing it alters the string source and can break dynamic execution.
  • When len() is part of a larger expression like 'len(a) + len(b)', rewriting only one side changes arithmetic meaning.
  • If the sequence variable is reused later and mutated, evaluating it twice (once for len, once for truthiness) could differ if the object's __len__ has side effects.
  • Rewriting drops any inline or trailing comments attached to the len() call, violating comment preservation expectations.
  • In a try/except block, if len() raises AttributeError on a None, rewriting to 'seq' raises the same error earlier but may shift blame line numbers.
  • If the sequence is a property with a getter that has side effects, calling it twice (once in original len, once in rewrite) vs once changes behavior.
  • In a boolean context wrapped by parentheses like '(len(x))', dropping len alters grouping if the expression is used in a bitwise context like '&'.
  • When len() compares to a non-zero constant like 'len(x) == 3', the fix incorrectly replaces it with 'x == 3' which is invalid.

Programs

idresulttitlehazard
r1-p001fix okRuntime-evaluated type annotationtype annotations evaluated at runtime, typing-only imports
r1-p002silent prediction offNon-zero constant comparison misfireWhen len() compares to a non-zero constant like 'len(x) == 3', the fix incorrectly replaces it with 'x == 3' which is invalid.
r1-p003silent prediction offLen on non-sequence typelen() on a non-sequence type like int has no truthiness equivalent, causing TypeError if rewritten.
r1-p004fix okKeyword-only and defaults evaluated oncekeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r1-p005fix okParentheses in bitwise contextIn a boolean context wrapped by parentheses like '(len(x))', dropping len alters grouping if the expression is used in a bitwise context like '&'.
r1-p006silentPathlib vs os.path behaviorpathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths
r1-p007silent prediction offSequence reused and mutatedIf the sequence variable is reused later and mutated, evaluating it twice (once for len, once for truthiness) could differ if the object's __len__ has side effects.
r1-p008fix okExceptions inside rewritten code, finally/elseexceptions raised inside the rewritten code, finally/else clauses, exception chaining
r2-p001silent prediction offLambda scope capture issuelen() inside a lambda or comprehension may capture different variables if rewritten naively due to scope changes.
r2-p002silent prediction offParentheses alter grouping in bitwise contextIn a boolean context wrapped by parentheses like '(len(x))', dropping len alters grouping if the expression is used in a bitwise context like '&'.
r2-p003silent prediction offShadowed builtins and redefined namesshadowed builtins and names redefined later in the module
r2-p004silent prediction offComments and docstrings on removed codecomments and docstrings attached to the removed or moved code
r2-p005silent prediction offCustom __bool__ vs __len__ and Nonetruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r2-p006silent prediction offEvaluation order and side effectsevaluation order and side effects when expressions are merged, duplicated or deleted
r2-p007silent prediction offExceptions inside rewritten code with finally/elseexceptions raised inside the rewritten code, finally/else clauses, exception chaining
r2-p008silent prediction offInt/float/bool coercion and Decimalint/float/bool coercion, Decimal, division and rounding

Sources

r1-p001 · Runtime-evaluated type annotation
1from typing import List
2T = List[int]
3def f() -> T:
4 x = [1, 2]
5 if len(x):
6 return x.pop()
7print(f(), f())
--- orig.py	2026-10-17 16:57:59.693886061 -0400
+++ fixed.py	2026-10-17 16:57:59.872888479 -0400
@@ -2,6 +2,6 @@
 T = List[int]
 def f() -> T:
   x = [1, 2]
-  if len(x):
+  if x:
     return x.pop()
 print(f(), f())
2 2
r1-p002 · Non-zero constant comparison misfire
1def g():
2 x = [1, 2, 3]
3 if len(x) == 3:
4 return sum(x)
5 return 0
6print(g())
7x = [1, 2]
8if len(x) == 3:
9 print('no')
10else:
11 print(len(x))
6
2
r1-p003 · Len on non-sequence type
1class A:
2 def __len__(self):
3 return 5
4a = A()
5try:
6 if len(a):
7 print('ok')
8except Exception as e:
9 print(type(e).__name__, e)
ok
r1-p004 · Keyword-only and defaults evaluated once
1def h(*, z=None):
2 if z is None:
3 z = []
4 if len(z):
5 z.append(1)
6 return z
7print(h())
8print(h(z=[2]))
--- orig.py	2026-10-17 16:58:00.241893460 -0400
+++ fixed.py	2026-10-17 16:58:00.404895660 -0400
@@ -1,7 +1,7 @@
 def h(*, z=None):
   if z is None:
     z = []
-  if len(z):
+  if z:
     z.append(1)
   return z
 print(h())
[]
[2, 1]
r1-p005 · Parentheses in bitwise context
1x = [1]
2y = [2]
3if (len(x)) & (len(y)):
4 print('both')
5else:
6 print(int(bool(len(x))) & int(bool(len(y))))
7x = []
8if (len(x)) & 1:
9 print('yes')
10else:
11 print('no')
--- orig.py	2026-10-17 16:58:00.500896956 -0400
+++ fixed.py	2026-10-17 16:58:00.665899183 -0400
@@ -3,7 +3,7 @@
 if (len(x)) & (len(y)):
   print('both')
 else:
-  print(int(bool(len(x))) & int(bool(len(y))))
+  print(int(bool(x)) & int(bool(y)))
 x = []
 if (len(x)) & 1:
   print('yes')
both
no
r1-p006 · Pathlib vs os.path behavior
1import os
2try:
3 os.mkdir('tmpdir')
4except FileExistsError:
5 pass
6try:
7 os.symlink('tmpdir', 'link')
8except FileExistsError:
9 pass
10p = os.path.exists('link')
11print(p)
12import shutil
13shutil.rmtree('tmpdir', ignore_errors=True)
14os.unlink('link')
True
r1-p007 · Sequence reused and mutated
1class Seq:
2 def __init__(self):
3 self.data = [1]
4 self.calls = 0
5 def __len__(self):
6 self.calls += 1
7 return len(self.data)
8s = Seq()
9if len(s):
10 s.data.append(2)
11print(s.calls, len(s.data))
12if len(s):
13 print('again')
1 2
again
r1-p008 · Exceptions inside rewritten code, finally/else
1def f():
2 x = [1]
3 try:
4 if len(x):
5 raise ValueError('bad')
6 except ValueError as e:
7 print('caught', e)
8 else:
9 print('no ex')
10 finally:
11 print('finally')
12f()
--- orig.py	2026-10-17 16:58:01.067904609 -0400
+++ fixed.py	2026-10-17 16:58:01.231906821 -0400
@@ -1,7 +1,7 @@
 def f():
   x = [1]
   try:
-    if len(x):
+    if x:
       raise ValueError('bad')
   except ValueError as e:
     print('caught', e)
caught bad
finally
r2-p001 · Lambda scope capture issue
1def make_funcs():
2 x = []
3 funcs = [lambda: len(x) for _ in range(2)]
4 x.append(1)
5 return [f() for f in funcs]
6
7print(make_funcs())
[1, 1]
r2-p002 · Parentheses alter grouping in bitwise context
1x = [1, 2]
2y = [3]
3result = (len(x)) & (len(y))
4print(result)
0
r2-p003 · Shadowed builtins and redefined names
1len = lambda s: 42 # shadow
2x = [1, 2, 3]
3if len(x):
4 print('truthy')
5else:
6 print('falsy')
truthy
r2-p004 · Comments and docstrings on removed code
1def process(items):
2 # Check if we have data
3 if len(items): # important check
4 return 'yes'
5 return 'no'
6
7print(process([1]))
yes
r2-p005 · Custom __bool__ vs __len__ and None
1class Weird:
2 def __len__(self): return 1
3 def __bool__(self): return False
4
5w = Weird()
6print('len(w):', len(w))
7print('bool(w):', bool(w))
8if len(w):
9 print('len truthy')
10else:
11 print('len falsy')
len(w): 1
bool(w): False
len truthy
r2-p006 · Evaluation order and side effects
1counter = 0
2def get_list():
3 global counter
4 counter += 1
5 return [1] * counter
6
7if len(get_list()) > 0:
8 print('first:', counter)
9if len(get_list()) > 0:
10 print('second:', counter)
first: 1
second: 2
r2-p007 · Exceptions inside rewritten code with finally/else
1def faulty_len(obj):
2 raise ValueError('bad')
3
4x = []
5try:
6 if len(faulty_len(x)):
7 pass
8except ValueError as e:
9 print('caught:', e)
10finally:
11 print('finally')
caught: bad
finally
r2-p008 · Int/float/bool coercion and Decimal
1from decimal import Decimal
2x = Decimal('0.0')
3if len([x]) == 0:
4 print('empty')
5else:
6 print('has items')
has items