ruff FURB188
labelled unknown
Analysis Nemotron 3 Super
Flags conditional slicing to remove prefixes/suffixes after startswith/endswith checks.
Fix: Replace conditional slice assignment with direct call to str.removeprefix() or str.removesuffix() using the same prefix/suffix literal.
12 hazards
- Rewriting s = s[:-len(p)] if s.startswith(p) else s when p is a variable with side effects in its evaluation changes timing of those effects.
- If the slice expression uses a variable whose value changes between the startswith check and the slice, the fix may use a stale value.
- When the assigned variable appears elsewhere in the condition (e.g., s = s[:-len(s)] if s.endswith(s) else s), the fix incorrectly binds the method to the original value.
- If the slice is part of a larger expression (e.g., f(s[:-len(p)] if s.startswith(p) else s)), extracting the slice changes evaluation order relative to other function arguments.
- If the branch contains commented-out code (e.g., s = s[:-len(p)] # TODO: verify if s.startswith(p) else s), the fix may discard those comments.
- When the target of assignment is a subscript or attribute (e.g., lst[i] = lst[i][:-len(p)] if lst[i].startswith(p) else lst[i]), the fix may not preserve the same assignment target semantics.
- If the startswith/endswith argument is not a literal but a complex expression, the fix may incorrectly assume it is safe to pass directly to removeprefix/removesuffix.
- In code with multiple assignments to the same variable in one line (e.g., a = b = s[:-len(p)] if s.startswith(p) else s), the fix may not replicate the multiple-assignment behavior.
- If the conditional is nested inside a lambda or comprehension, the fix may alter scoping by lifting expressions.
- When the string method is called on a non-str object that has startswith/endswith but not removeprefix/removesuffix (e.g., bytes), the fix introduces an AttributeError.
- If the string is a subclass that overrides removeprefix/removesuffix with different behavior, the fix changes semantics.
- In Python versions before 3.9, the fix would raise an AttributeError since removeprefix/removesuffix do not exist.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | FURB188 Hazard 1: Variable mutated between check and slice | If the slice expression uses a variable whose value changes between the startswith check and the slice, the fix may use a stale value. |
| r1-p002 | silent prediction off | FURB188 Hazard 2: Non-str object with startswith but no removeprefix | When the string method is called on a non-str object that has startswith/endswith but not removeprefix/removesuffix (e.g., bytes), the fix introduces an AttributeError. |
| r1-p003 | silent prediction off | FURB188 Hazard 3: Truthiness and falsy values | truthiness of custom __bool__/__len__, None vs falsy, 'is' vs '==' |
| r1-p004 | silent prediction off | FURB188 Hazard 4: Shadowed builtins | shadowed builtins and names redefined later in the module |
| r1-p005 | invalid | FURB188 Hazard 5: Commented-out code discarded by fix | If the branch contains commented-out code (e.g., s = s[:-len(p)] # TODO: verify if s.startswith(p) else s), the fix may discard those comments. |
| r1-p006 | fix ok | FURB188 Hazard 6: Assigned variable appears in condition | When the assigned variable appears elsewhere in the condition (e.g., s = s[:-len(s)] if s.endswith(s) else s), the fix incorrectly binds the method to the original value. |
| r1-p007 | silent | FURB188 Hazard 7: Type annotations evaluated at runtime | type annotations evaluated at runtime, typing-only imports |
| r1-p008 | silent prediction off | FURB188 Hazard 8: Python < 3.9 lacks removeprefix/removesuffix | In Python versions before 3.9, the fix would raise an AttributeError since removeprefix/removesuffix do not exist. |
| r2-p001 | fix ok | Star import and module-level mutation affecting global used in slicing | star-imports, module-level side effects, __all__ |
| r2-p002 | fix ok | Mutable default argument aliasing after function call | mutable default arguments and aliasing after a rewrite |
| r2-p003 | fix ok | String formatting with f-string and repr in slice length | string formatting edge cases: %, format, f-strings, repr vs str, nested quotes |
| r2-p004 | silent prediction off | Subclass overriding removeprefix with different behavior | If the string is a subclass that overrides removeprefix/removesuffix with different behavior, the fix changes semantics. |
| r2-p005 | fix ok | Type annotation evaluated at runtime using forward reference | type annotations evaluated at runtime, typing-only imports |
| r2-p006 | fix ok | Keyword-only argument with default evaluated once | keyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once |
| r2-p007 | silent prediction off | Assigned variable appears in condition via function call | When the assigned variable appears elsewhere in the condition (e.g., s = s[:-len(s)] if s.endswith(s) else s) |
| r2-p008 | invalid | Conditional inside list comprehension altering scoping | If the conditional is nested inside a lambda or comprehension, the fix may alter scoping by lifting expressions. |
Sources
r1-p001 · FURB188 Hazard 1: Variable mutated between check and slice
1p = 'pre'
2s = 'prefix'
3if s.startswith(p):
4 p = 'x' # mutate p after check
5 s = s[:-len(p)] if s.startswith(p) else s
6print(s)
prefix
r1-p002 · FURB188 Hazard 2: Non-str object with startswith but no removeprefix
1b = b'prefix'
2p = b'pre'
3try:
4 if b.startswith(p):
5 b = b[:-len(p)] if b.startswith(p) else b
6 print(b)
7except AttributeError as e:
8 print(f'{type(e).__name__}: {e}')
b'pre'
r1-p003 · FURB188 Hazard 3: Truthiness and falsy values
1class C:
2 def __len__(self): return 0
3 def startswith(self, _): return True
4 def endswith(self, _): return False
5c = C()
6try:
7 if c.startswith('pre'):
8 c = c[:-3] if c.startswith('pre') else c
9 print(len(c))
10except Exception as e:
11 print(f'{type(e).__name__}: {e}')
TypeError: 'C' object is not subscriptable
r1-p004 · FURB188 Hazard 4: Shadowed builtins
1str = lambda x: x.upper()
2s = 'prefix'
3p = 'pre'
4if s.startswith(p):
5 s = s[:-len(p)] if s.startswith(p) else s
6print(str(s))
PRE
r1-p005 · FURB188 Hazard 5: Commented-out code discarded by fix (generated program does not parse: Traceback (most recent call last):)
1p = 'pre'
2s = 'prefix'
3# This comment should be preserved
4if s.startswith(p):
5 s = s[:-len(p)] # TODO: verify
6 if s.startswith(p) else s
7print(s)
r1-p006 · FURB188 Hazard 6: Assigned variable appears in condition
1s = 'suffixfix'
2if s.endswith(s):
3 s = s[:-len(s)] if s.endswith(s) else s
4print(repr(s))
--- orig.py 2026-10-17 17:37:41.288565904 -0400 +++ fixed.py 2026-10-17 17:37:41.447564382 -0400 @@ -1,4 +1,4 @@ s = 'suffixfix' if s.endswith(s): - s = s[:-len(s)] if s.endswith(s) else s + s = s.removesuffix(s) print(repr(s))
''
r1-p007 · FURB188 Hazard 7: Type annotations evaluated at runtime
1from typing import List
2s: str = 'prefix'
3p: str = 'pre'
4if s.startswith(p):
5 s = s[:-len(p)] if s.startswith(p) else s
6print(s)
pre
r1-p008 · FURB188 Hazard 8: Python < 3.9 lacks removeprefix/removesuffix
1import sys
2s = 'prefix'
3p = 'pre'
4if s.startswith(p):
5 try:
6 s = s.removeprefix(p)
7 except AttributeError:
8 s = s[len(p):]
9print(s)
fix
r2-p001 · Star import and module-level mutation affecting global used in slicing
1prefix = 'ab'
2s = 'abcde'
3if s.startswith(prefix):
4 s = s[len(prefix):]
5print(s)
--- orig.py 2026-10-17 17:37:45.769523104 -0400 +++ fixed.py 2026-10-17 17:37:45.933521541 -0400 @@ -1,5 +1,4 @@ prefix = 'ab' s = 'abcde' -if s.startswith(prefix): - s = s[len(prefix):] +s = s.removeprefix(prefix) print(s)
cde
r2-p002 · Mutable default argument aliasing after function call
1def f(x, lst=[]):
2 lst.append(x)
3 return lst
4prefix = 'pre'
5s = 'prefix'
6if s.startswith(prefix):
7 s = s[len(prefix):]
8f(s)
9print(f.__defaults__[0])
--- orig.py 2026-10-17 17:37:46.042520502 -0400 +++ fixed.py 2026-10-17 17:37:46.205518950 -0400 @@ -3,7 +3,6 @@ return lst prefix = 'pre' s = 'prefix' -if s.startswith(prefix): - s = s[len(prefix):] +s = s.removeprefix(prefix) f(s) print(f.__defaults__[0])
['fix']
r2-p003 · String formatting with f-string and repr in slice length
1x = 'hello'
2pref = 'h'
3if x.startswith(pref):
4 x = x[len(pref):]
5print(f'Result: {x!r}')
--- orig.py 2026-10-17 17:37:46.308517970 -0400 +++ fixed.py 2026-10-17 17:37:46.476516370 -0400 @@ -1,5 +1,4 @@ x = 'hello' pref = 'h' -if x.startswith(pref): - x = x[len(pref):] +x = x.removeprefix(pref) print(f'Result: {x!r}')
Result: 'ello'
r2-p004 · Subclass overriding removeprefix with different behavior
1class MyStr(str):
2 def removeprefix(self, p):
3 return self + p # intentional break
4s = MyStr('test')
5pref = 'te'
6if s.startswith(pref):
7 s = s[len(pref):]
8else:
9 s = s.removeprefix(pref)
10print(type(s), s)
<class 'str'> st
r2-p005 · Type annotation evaluated at runtime using forward reference
1from __future__ import annotations
2prefix: str = 'xyz'
3s: str = 'xyz123'
4if s.startswith(prefix):
5 s = s[len(prefix):]
6print(s)
--- orig.py 2026-10-17 17:37:46.710514142 -0400 +++ fixed.py 2026-10-17 17:37:46.879512533 -0400 @@ -1,6 +1,5 @@ from __future__ import annotations prefix: str = 'xyz' s: str = 'xyz123' -if s.startswith(prefix): - s = s[len(prefix):] +s = s.removeprefix(prefix) print(s)
123
r2-p006 · Keyword-only argument with default evaluated once
1def g(*, cut='ab', s='abcd'):
2 if s.startswith(cut):
3 s = s[len(cut):]
4 return s
5print(g())
6print(g(s='abab'))
--- orig.py 2026-10-17 17:37:46.976511609 -0400 +++ fixed.py 2026-10-17 17:37:47.134510106 -0400 @@ -1,6 +1,5 @@ def g(*, cut='ab', s='abcd'): - if s.startswith(cut): - s = s[len(cut):] + s = s.removeprefix(cut) return s print(g()) print(g(s='abab'))
cd ab
r2-p007 · Assigned variable appears in condition via function call
1s = 'hello'
2if s.endswith(s[-1:]): # s appears in condition
3 s = s[:-1]
4print(s)
hell
r2-p008 · Conditional inside list comprehension altering scoping (generated program does not parse: Traceback (most recent call last):)
1prefix = 'a'
2words = ['apple', 'banana', 'avocado']
3result = [w[len(prefix):] if w.startswith(prefix) else w for w in prefix := ['a'] * 3][0]
4print(result)