ruff PIE810
labelled unknown
Analysis Nemotron 3 Super
Flags repeated startswith/endswith calls on the same string value combined with 'or'/'and'.
Fix: Replaces repeated startswith/endswith calls on the same value combined with logical operators by a single call with a tuple of all arguments.
10 hazards
- If any argument is already a tuple, wrapping it in another tuple creates nested tuples causing TypeError at runtime.
- If the repeated value expression has side effects (e.g., a function call), evaluating it once instead of multiple times changes behavior.
- If the repeated value is a mutable object and later mutated between calls, the fix uses the original value for all checks.
- If an argument is a variable whose type cannot be statically determined to be str, the resulting tuple may contain non-str elements causing runtime TypeError.
- If the logical operator is 'and' and the arguments are not mutually exclusive, the fix may alter short-circuit evaluation semantics.
- Comments attached to individual startswith/endswith calls may be lost during rewrite.
- If the repeated value is a property with side effects, accessing it once vs multiple times changes behavior.
- If the code relies on the specific AttributeError from a non-string object having no startswith, the fix may delay or change the error.
- If the repeated value is a subclass of str that overrides startswith with different semantics, the fix may not preserve behavior due to tuple handling differences.
- If an argument is a string-like object (e.g., bytes, bytearray) where startswith expects different types, the tuple may cause type mismatch errors.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fired, no fix | Shadowed Built-in Override | shadowed builtins and names redefined later in the module |
| r1-p002 | invalid | Comment Loss During Rewrite | Comments attached to individual startswith/endswith calls may be lost during rewrite. |
| r1-p003 | fired, no fix | Non-str Argument TypeError | If an argument is a variable whose type cannot be statically determined to be str, the resulting tuple may contain non-str elements causing runtime TypeError. |
| r1-p004 | invalid | Attached Docstring/Comment Loss | comments and docstrings attached to the removed or moved code |
| r1-p005 | fired, no fix | File Write Side Effect Interference | file modes and truncation, partial writes, non-str data passed to write |
| r1-p006 | fired, no fix | Write()-Only Context Manager | context managers that are not files, objects with write() but no close() |
| r1-p007 | fired, no fix | Bytes/String Mismatch in Tuple | If an argument is a string-like object (e.g., bytes, bytearray) where startswith expects different types, the tuple may cause type mismatch errors. |
| r1-p008 | fired, no fix prediction off | Runtime Type Annotation Evaluation | type annotations evaluated at runtime, typing-only imports |
| r2-p001 | fired, no fix | bytes vs str encoding newline | bytes vs str, encoding and newline handling, text vs binary modes |
| r2-p002 | fired, no fix | comment loss during rewrite | Comments attached to individual startswith/endswith calls may be lost during rewrite. |
| r2-p003 | fired, no fix | mutable object mutated between calls | If the repeated value is a mutable object and later mutated between calls, the fix uses the original value for all checks. |
| r2-p004 | silent prediction off | walrus comprehension lambda scoping | walrus, comprehensions scoping, late binding in closures and lambdas |
| r2-p005 | fired, no fix | string formatting edge cases | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p006 | fired, no fix | keyword-only args forwarded | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p007 | fired, no fix prediction off | type annotations evaluated at runtime | type annotations evaluated at runtime, typing-only imports |
| r2-p008 | fired, no fix | mutable default arguments aliasing | mutable default arguments and aliasing after a rewrite |
Sources
r1-p001 · Shadowed Built-in Override
1startswith = lambda x, y: x[:len(y)] == y
2s = 'hello'
3if s.startswith('h') or s.startswith('he'):
4 print('matched', s)
5s = 'world'
6print(startswith(s, 'wo'))
matched hello True
r1-p002 · Comment Loss During Rewrite (generated program does not parse: Traceback (most recent call last):)
1s = 'test'
2# prefix check
3if s.startswith('t') # should pass
4 or s.startswith('te'): # redundant
5 print('ok')
6else:
7 print('fail')
r1-p003 · Non-str Argument TypeError
1def f(): return 1
2x = 'abc'
3try:
4 if x.startswith('a') or x.startswith(f()):
5 print('yes')
6except Exception as e:
7 print(type(e).__name__, e)
yes
r1-p004 · Attached Docstring/Comment Loss (generated program does not parse: Traceback (most recent call last):)
1"""Module doc"""
2x = 'abc'
3# need prefix
4def check():
5 if x.startswith('a') # first
6 or x.startswith('ab'): # second
7 return True
8 return False
9print(check())
r1-p005 · File Write Side Effect Interference
1s = 'log'
2if s.startswith('l') or s.startswith('lo'):
3 with open('tmp.txt', 'w') as f:
4 f.write('x')
5 f.write('y')
6with open('tmp.txt', 'r') as f:
7 print(f.read())
8import os
9os.remove('tmp.txt')
xy
r1-p006 · Write()-Only Context Manager
1class Writer:
2 def __init__(self): self.buf = ''
3 def write(self, data): self.buf += data
4 def __enter__(self): return self
5 def __exit__(self, *a): pass
6s = 'msg'
7if s.startswith('m') or s.startswith('mo'):
8 with Writer() as w:
9 w.write('ok')
10 print(writer.buf if 'writer' in locals() else 'none')
none
r1-p007 · Bytes/String Mismatch in Tuple
1s = b'hello'
2try:
3 if s.startswith(b'h') or s.startswith(b'he'):
4 print('match')
5except Exception as e:
6 print(type(e).__name__, e)
match
r1-p008 · Runtime Type Annotation Evaluation
1from typing import List
2s: str = 'test'
3if s.startswith('t') or s.startswith('te'):
4 print('ok')
5else:
6 print('fail')
ok
r2-p001 · bytes vs str encoding newline
1data = b'\r\nhello\n'
2if data.endswith(b'\n') or data.endswith(b'\r\n'):
3 print('line ends:', data[-2:])
4else:
5 print('no match')
line ends: b'o\n'
r2-p002 · comment loss during rewrite
1s = 'apple'
2# check for fruit prefix
3if s.startswith('a') or s.startswith('b'): # common starters
4 print('starts with a or b')
5else:
6 print('other')
starts with a or b
r2-p003 · mutable object mutated between calls
1lst = ['a', 'b']
2s = str(lst)
3if s.startswith('[') or s.startswith('[a'):
4 print('first:', s)
5lst.append('c')
6s = str(lst)
7if s.startswith('[') or s.startswith('[a'):
8 print('second:', s)
first: ['a', 'b'] second: ['a', 'b', 'c']
r2-p004 · walrus comprehension lambda scoping
1parts = ['sun', 'moon', 'star']
2results = []
3for p in parts:
4 if (s := p).startswith('s') or s.startswith('m'):
5 results.append(s)
6print('matched:', results)
matched: ['sun', 'moon', 'star']
r2-p005 · string formatting edge cases
1name = "alice"
2greeting = f'Hello, {name}!'
3if greeting.startswith('H') or greeting.startswith('Hello'):
4 print('greeting ok')
5else:
6 print('unexpected')
greeting ok
r2-p006 · keyword-only args forwarded
1def check(s, *, prefix='a'):
2 if s.startswith(prefix) or s.startswith('b'):
3 return True
4 return False
5print(check('apple'))
6print(check('banana'))
True True
r2-p007 · type annotations evaluated at runtime
1from typing import List
2s: str = 'test'
3if s.startswith('t') or s.startswith('e'):
4 print('starts with t or e')
5else:
6 print('other')
starts with t or e
r2-p008 · mutable default arguments aliasing
1def process(items, seen=[]):
2 for item in items:
3 if item.startswith('a') or item.startswith('b'):
4 seen.append(item)
5 return seen
6print(process(['apple', 'bird']))
7print(process(['cat', 'dog']))
['apple', 'bird'] ['apple', 'bird']