You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG] Text negative integer indexing drops span styles #4237
I've checked the docs and closed issues for possible solutions.
I can't find my issue in the FAQ.
Describe the bug
Indexing styled Text with a negative integer returns the correct character but loses its spans. Positive indexing and a negative slice preserve the style. Python's negative-index semantics suggest text[-1] should be equivalent to text[len(text) - 1], including formatting.
o [Span(0, 1, 'bold')]
o []
llo [Span(0, 3, 'bold')]
Expected: text[-1] returns a bold o like text[4].
At rich/text.py:198-209, __getitem__ uses the negative offset for Python string indexing (which succeeds), but compares that same negative offset with nonnegative span boundaries (which never match). Indexing once to keep Python's IndexError behavior and then normalizing the negative offset before checking spans appears to fix it. Could @willmcgugan confirm that preserving spans for negative integer indices is the intended behavior? I have not opened a PR pending the approval required by AI_POLICY.md.
Platform
Windows, CPython 3.10.1, Rich 15.0.0 installed editable from default-branch commit 9d8f9a372cc5916fd4781fec207ced7ddac2f08f. Pure API example; terminal rendering not required. Searched open and closed issues/PRs for negative Text indexes and lost styles without finding a matching report.
AI disclosure: bug discovery assisted by Luna (timotty-responses/luna); reproduction and proposed fix reviewed with OpenCode (GPT-6 Sol).
Hi, I've reproduced this locally and put together a minimal fix with a regression test.
Root cause
Negative integer indexing retrieves the correct character from self.plain, but span matching still uses the original negative offset. Since span boundaries are non-negative, the styles are lost.
Fix
In rich/text.py, inside Text.getitem(), I retrieve the character first, then normalize the negative offset before checking spans:
character = self.plain[offset]
if offset < 0:
offset += len(self.plain)
This preserves Python's normal IndexError behavior while ensuring negative indices match existing spans correctly.
tests/test_text.py: 113 passed
Full test suite: 956 passed, 25 skipped, 0 failed
git diff --check: passed
Changes are limited to rich/text.py and tests/test_text.py
I confirmed that the original bug occurs before the fix and that the regression test passes after applying it.
Would this approach be acceptable for an upstream contribution? Since PR creation is currently restricted to collaborators, I'd appreciate guidance on how to proceed in accordance with the repository's contribution policy.
text[4] contains b with [Span(0, 1, 'red'), Span(0, 1, 'bold')] in that order, while text[-7] contains the same character with no spans. All 11 corresponding positive/negative pairs in this sample lose their styles; text[-7:-4] preserves both spans for bar. A nested/crossing three-style sample showed the same mismatch at eight styled positions, while its two unformatted edges agreed. Positive indexing preserved span order. Empty text and out-of-range indices still raised IndexError in 15 checks.
The existing tests/test_text.py passed on unchanged source (113 tests, CPython 3.12.14, Linux). No candidate fix or full suite was tested. The documented Poetry install hit a stale-lock error; the checks used the lock's exact dependency versions and hashes in an isolated environment.
AI-assisted validation and draft. This supplements Timotty's report and mikegit123git's proposed approach.
Activity
mikegit123git commentedon Oct 15, 2026
Hi, I've reproduced this locally and put together a minimal fix with a regression test.
Root cause
Negative integer indexing retrieves the correct character from self.plain, but span matching still uses the original negative offset. Since span boundaries are non-negative, the styles are lost.
Fix
In rich/text.py, inside Text.getitem(), I retrieve the character first, then normalize the negative offset before checking spans:
character = self.plain[offset]
if offset < 0:
offset += len(self.plain)
This preserves Python's normal IndexError behavior while ensuring negative indices match existing spans correctly.
Regression test
Added a test in tests/test_text.py:
negative = Text.from_markup("[bold]hello[/bold]")
assert negative[-1] == Text("o", spans=[Span(0, 1, "bold")])
Local validation
tests/test_text.py: 113 passed
Full test suite: 956 passed, 25 skipped, 0 failed
git diff --check: passed
Changes are limited to rich/text.py and tests/test_text.py
I confirmed that the original bug occurs before the fix and that the regression test passes after applying it.
Would this approach be acceptable for an upstream contribution? Since PR creation is currently restricted to collaborators, I'd appreciate guidance on how to proceed in accordance with the repository's contribution policy.
Thanks!
hafizahmedzia commentedon Oct 17, 2026
Additional span-boundary evidence on unchanged
9d8f9a37, beyond the final-character case already discussed:text[4]containsbwith[Span(0, 1, 'red'), Span(0, 1, 'bold')]in that order, whiletext[-7]contains the same character with no spans. All 11 corresponding positive/negative pairs in this sample lose their styles;text[-7:-4]preserves both spans forbar. A nested/crossing three-style sample showed the same mismatch at eight styled positions, while its two unformatted edges agreed. Positive indexing preserved span order. Empty text and out-of-range indices still raisedIndexErrorin 15 checks.The existing
tests/test_text.pypassed on unchanged source (113 tests, CPython 3.12.14, Linux). No candidate fix or full suite was tested. The documented Poetry install hit a stale-lock error; the checks used the lock's exact dependency versions and hashes in an isolated environment.AI-assisted validation and draft. This supplements Timotty's report and mikegit123git's proposed approach.