ruff PLC1802
labelled unknown
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
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Runtime-evaluated type annotation | type annotations evaluated at runtime, typing-only imports |
| r1-p002 | silent prediction off | Non-zero constant comparison misfire | When len() compares to a non-zero constant like 'len(x) == 3', the fix incorrectly replaces it with 'x == 3' which is invalid. |
| r1-p003 | silent prediction off | Len on non-sequence type | len() on a non-sequence type like int has no truthiness equivalent, causing TypeError if rewritten. |
| r1-p004 | fix ok | Keyword-only and defaults evaluated once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r1-p005 | fix ok | Parentheses in bitwise context | In a boolean context wrapped by parentheses like '(len(x))', dropping len alters grouping if the expression is used in a bitwise context like '&'. |
| r1-p006 | silent | Pathlib vs os.path behavior | pathlib vs os.path behavior on relative paths, symlinks, missing files, bytes paths |
| r1-p007 | silent prediction off | Sequence reused and mutated | 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. |
| r1-p008 | fix ok | Exceptions inside rewritten code, finally/else | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p001 | silent prediction off | Lambda scope capture issue | len() inside a lambda or comprehension may capture different variables if rewritten naively due to scope changes. |
| r2-p002 | silent prediction off | Parentheses alter grouping in bitwise context | In a boolean context wrapped by parentheses like '(len(x))', dropping len alters grouping if the expression is used in a bitwise context like '&'. |
| r2-p003 | silent prediction off | Shadowed builtins and redefined names | shadowed builtins and names redefined later in the module |
| r2-p004 | silent prediction off | Comments and docstrings on removed code | comments and docstrings attached to the removed or moved code |
| r2-p005 | silent prediction off | Custom __bool__ vs __len__ and None | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r2-p006 | silent prediction off | Evaluation order and side effects | evaluation order and side effects when expressions are merged, duplicated or deleted |
| r2-p007 | silent prediction off | Exceptions inside rewritten code with finally/else | exceptions raised inside the rewritten code, finally/else clauses, exception chaining |
| r2-p008 | silent prediction off | Int/float/bool coercion and Decimal | int/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