fixhunt

ruff FURB188

labelled unknown
Runs / 2026-10-17T21-37-31-641Z-1cbu-ruff-FURB188ruff 0.17.12026-10-17 21:37 UTC8 programs × 2 round(s)16 programs, 6 fired, 6 fixed, 0 findings, 2 rejectedmodel cost $0.0032

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

idresulttitlehazard
r1-p001silent prediction offFURB188 Hazard 1: Variable mutated between check and sliceIf the slice expression uses a variable whose value changes between the startswith check and the slice, the fix may use a stale value.
r1-p002silent prediction offFURB188 Hazard 2: Non-str object with startswith but no removeprefixWhen 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-p003silent prediction offFURB188 Hazard 3: Truthiness and falsy valuestruthiness of custom __bool__/__len__, None vs falsy, 'is' vs '=='
r1-p004silent prediction offFURB188 Hazard 4: Shadowed builtinsshadowed builtins and names redefined later in the module
r1-p005invalidFURB188 Hazard 5: Commented-out code discarded by fixIf 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-p006fix okFURB188 Hazard 6: Assigned variable appears in conditionWhen 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-p007silentFURB188 Hazard 7: Type annotations evaluated at runtimetype annotations evaluated at runtime, typing-only imports
r1-p008silent prediction offFURB188 Hazard 8: Python < 3.9 lacks removeprefix/removesuffixIn Python versions before 3.9, the fix would raise an AttributeError since removeprefix/removesuffix do not exist.
r2-p001fix okStar import and module-level mutation affecting global used in slicingstar-imports, module-level side effects, __all__
r2-p002fix okMutable default argument aliasing after function callmutable default arguments and aliasing after a rewrite
r2-p003fix okString formatting with f-string and repr in slice lengthstring formatting edge cases: %, format, f-strings, repr vs str, nested quotes
r2-p004silent prediction offSubclass overriding removeprefix with different behaviorIf the string is a subclass that overrides removeprefix/removesuffix with different behavior, the fix changes semantics.
r2-p005fix okType annotation evaluated at runtime using forward referencetype annotations evaluated at runtime, typing-only imports
r2-p006fix okKeyword-only argument with default evaluated oncekeyword-only, positional-only, *args/**kwargs forwarding, defaults evaluated once
r2-p007silent prediction offAssigned variable appears in condition via function callWhen the assigned variable appears elsewhere in the condition (e.g., s = s[:-len(s)] if s.endswith(s) else s)
r2-p008invalidConditional inside list comprehension altering scopingIf 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)