Today I learned (and observed empirically across 100 benchmarked automated program repairs) why Python’s str.rstrip() is one of the most common sources of silent string corruption in production codebases—and why small code language models are fundamentally blind to it.
The Bug: What Does rstrip() Actually Do?
Consider this seemingly innocuous helper function to clean up domain names:
def strip_domain_suffix(domain: str) -> str:
return domain.rstrip(".com")
If you test it with standard test inputs:
strip_domain_suffix("google.com") # -> "google" (Works!)
strip_domain_suffix("github.com") # -> "github" (Works!)
strip_domain_suffix("wikipedia.com") # -> "wikipedia" (Works!)
Everything looks fine. The developer commits the code, the unit test passes, and it ships to production.
Then a domain like "telecom.com" or "intercom.com" passes through:
strip_domain_suffix("telecom.com") # -> "tele" (BUG!)
strip_domain_suffix("intercom.com") # -> "inter" (BUG!)
strip_domain_suffix("dotcom.com") # -> "dot" (BUG!)
Where did the "com" in "telecom" go?
The Mechanism: Suffix vs. Character Set
In Python, str.rstrip([chars]) does not strip a string suffix. It strips any character present in the chars set from the right side of the string until it encounters a character that is not in that set.
When you write:
domain.rstrip(".com")
Python interprets the argument as the character set:
$$\text{chars} = { ‘.’, ‘c’, ‘o’, ‘m’ }$$
Let’s trace what happens to "telecom.com":
- Python inspects the last character:
'm'. Is'm'in{'.', 'c', 'o', 'm'}? Yes. Stripped. - Next:
'o'. In set? Yes. Stripped. - Next:
'c'. In set? Yes. Stripped. - Next:
'.'. In set? Yes. Stripped. - Next:
'm'(from"telecom"). In set? Yes! Stripped. - Next:
'o'. In set? Yes! Stripped. - Next:
'c'. In set? Yes! Stripped. - Next:
'e'. In set? No. Execution halts. - Final returned string:
"tele".
The Modern Fix: Python 3.9+ removesuffix()
To fix this once and for all, Python 3.9 introduced PEP 616 with two dedicated methods:
str.removesuffix(suffix)str.removeprefix(prefix)
Unlike rstrip(), removesuffix() treats the argument as a continuous substring tuple:
"telecom.com".removesuffix(".com") # -> "telecom"
"intercom.com".removesuffix(".com") # -> "intercom"
"api.internal".removesuffix(".com") # -> "api.internal" (no-op)
If you must support legacy Python (< 3.9):
def removesuffix(s: str, suffix: str) -> str:
if suffix and s.endswith(suffix):
return s[:-len(suffix)]
return s
Why Small Code LLMs Fail This 100% of the Time
While building the benchmark dataset for Patchwork (an automated program repair agent using 4-bit qwen2.5-coder:3b), I included string-stripping defects in the evaluation suite.
Out of 21 defect categories, rstrip misuse was one of six categories where the 3B model scored 0% Pass@overall across all reflection retries:
- Token Frequency Bias: Code training corpora contain immense volumes of pre-Python 3.9 code. When prompted to “strip suffix
.com”, the model’s highest probability token completion is almost always.rstrip(...). - Surface Plausibility: The model recognizes that
.rstripstands for “right strip” and pairs it with the string. Syntax-wise, it parses cleanly with zero linter errors. - Self-Written Test Hallucination: When asked to write a unit test to verify its fix, the model almost always tests
"example.com"->"example". The test passes in its sandboxed environment, convincing the agent that the patch succeeded.
Static Analysis Defense: Ruff & Flake8
Because human eyes and LLMs routinely glance past this bug, static analysis linters now flag it explicitly:
In Ruff (and flake8-bugbear), rule B005 checks for multi-character string literals passed to .strip(), .lstrip(), or .rstrip():
$ ruff check service.py
service.py:4:12: B005 Using `.rstrip(".com")` with multi-character argument looks like an attempt to remove a suffix. Use `.removesuffix(".com")` instead.
In autonomous coding agents, integrating static linters (like Ruff) directly into the reflection harness is essential to catch logical blind spots that unit tests overlook.