Skip to content

[BUG] Text negative integer indexing drops span styles #4237

Open

Description

@Timotty
  • 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.

from rich.text import Text

text = Text.from_markup("[bold]hello[/bold]")
print(text[4].plain, text[4].spans)
print(text[-1].plain, text[-1].spans)
print(text[-3:].plain, text[-3:].spans)

Actual output:

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).

Activity

  1. mikegit123git commented on Oct 15, 2026

    @mikegit123git

    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!

  2. hafizahmedzia commented on Oct 17, 2026

    @hafizahmedzia

    Additional span-boundary evidence on unchanged 9d8f9a37, beyond the final-character case already discussed:

    text = Text.from_markup("[red]foo [bold]bar[/red] baz[/bold]")

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions