Skip to content

[BUG] Traceback rendering fails on ParseError with a non-string message #4225

Open

Description

@saitakarcesme
  • Checked the traceback documentation and searched open/closed issues for SyntaxError, ParseError, and ExpatError.
  • Checked the FAQ.

Describe the bug

Rendering an xml.etree.ElementTree.ParseError whose message is an ExpatError raises a secondary TypeError, masking the original XML parse error. This occurs with the pure-Python ElementTree parser used by defusedxml (see tiran/defusedxml#105). The reproduction below needs only Rich and the standard library:

import io
import sys
from xml.etree.ElementTree import ParseError
from xml.parsers.expat import ExpatError

from rich.console import Console
from rich.traceback import Traceback

try:
    raise ParseError(ExpatError("syntax error"))
except ParseError:
    Console(file=io.StringIO()).print(Traceback.from_exception(*sys.exc_info()))

Actual result:

TypeError: str or Text instance required, not ExpatError('syntax error')

Expected: render the original exception without raising a new exception.

ParseError subclasses SyntaxError. Traceback.extract() currently stores exc_value.msg directly in _SyntaxError, although it already has a safe_str() helper for the main exception value. Would normalizing this field with that helper, with tests for non-string messages and a failing __str__, be an acceptable fix?

Platform

macOS arm64, CPython 3.13.5, Rich 15.0.0. Also reproduced using the current main checkout. Output is directed to StringIO, so no terminal-specific rendering is involved. The original end-to-end example also fails with defusedxml 0.8.0rc2 and structlog 26.1.0. Explicitly selecting structlog.dev.plain_traceback is a verified workaround for that integration.

This report and local reproduction were prepared with OpenAI Codex. Per the AI policy, no PR is being submitted without maintainer approval of the proposed solution.

Activity

  1. 5h4d0wn1k commented on Sep 23, 2026

    @5h4d0wn1k

    "Confirmed and reproduced locally (rich 15.0.0, CPython 3.14): Console().print(Traceback.from_exception(*sys.exc_info())) under ParseError(ExpatError(...)) raises TypeError: str or Text instance required, not ExpatError("syntax error").

    Root cause is a bit narrower than the title: the SyntaxError branch stores the raw message at rich/traceback.py:517 (msg=exc_value.msg, unnormalized), and render_stack feeds that field into ReprHighlighter at traceback.py:687, which only accepts str/Text. Normalizing at extraction is sufficient and minimal:

    msg=safe_str(exc_value.msg),
    

    safe_str already exists and is used for exc_value at line 486; for a standard SyntaxError the message is already a str, so this is a no-op.

    I also tested the adjacent sub-cases: an embedded-newlines message renders fine (no crash), while None and other non-str messages crash exactly as above.

    Per AI_POLICY.md an AI-assisted PR needs a maintainer to approve the solution on the issue first. Maintainers, would you be open to this one-line fix with a regression test alongside test_syntax_error in tests/test_traceback.py? Happy to submit once approved. @willmcgugan

    (Note: this reproduction/analysis was prepared with AI assistance)"

  2. alexprengere commented on Sep 25, 2026

    @alexprengere
    Contributor

    For the record I encountered the bug using PyPy on this MRE:

    import xml.etree.ElementTree as etree
    from rich.console import Console
    
    console = Console()
    try:
        xml = etree.XML("<")
    except Exception as e:
        console.print_exception() # TypeError: str or Text instance required,
                                  # not ExpatError('unclosed token: line 1, column 0')

    The reason is that in PyPy, the pure-Python version lxml sets the first argument of ParseError to a non-str, while in CPython, the C version of lxml casts it to a str.
    I opened a CPython issue to fix this upstream (as PyPy use CPython stdlib), but I still believe rich should not fail here, as you generally do not want your error path to error itself.

  3. alexprengere commented on Sep 26, 2026

    @alexprengere
    Contributor

    I pushed a fix in my fork here, but I cannot create a PR as I think those are limited to collaborators.

  4. EDfwy1 commented on Sep 27, 2026

    @EDfwy1

    Hi! I ran into this bug and dug into it. Root cause found, along with a minimal fix and a regression test — ready on my fork if a PR would be welcome.

    Root cause

    In rich/traceback.py, the Traceback._render_stack() handler builds a _SyntaxError from any SyntaxError subclass:

    stack.syntax_error = _SyntaxError(
        ...
        msg=exc_value.msg,
        ...
    )

    But SyntaxError.msg is not guaranteed to be a str. xml.etree.ElementTree.ParseError (and xml.parsers.expat.ExpatError) carry exception-specific msg values — when a ParseError is constructed from an ExpatError instance, msg is the ExpatError object itself, not a string. Rich's highlighter() requires str, so rendering crashes with:

    TypeError: str or Text instance required, not ExpatError('syntax error')
    

    Minimal repro

    from xml.etree.ElementTree import ParseError
    from xml.parsers.expat import ExpatError
    from rich.console import Console
    
    console = Console()
    try:
        raise ParseError(ExpatError("syntax error"))
    except ParseError:
        console.print_exception()  # TypeError

    Fix (one line)

    Coerce the message to str in rich/traceback.py:

    -        msg=exc_value.msg,
    +        msg=str(exc_value.msg),

    Regression test

    Added tests/test_traceback.py::test_syntax_error_non_str_message (raises a ParseError wrapping an ExpatError, prints the exception, asserts "ParseError" appears in the output). Passes locally; the traceback test suite is green with the change.

    Ready-to-merge branch

    Since PR creation on this repo is currently limited to collaborators, the complete fix (fix + regression test + CHANGELOG entry) is on my fork:

    Branch: EDfwy1:fix/traceback-non-str-syntax-message (commit 90a4936)

    If a maintainer could either open the PR from that branch, or grant me PR access, I'd be happy to submit it properly. Thanks!

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