- [x] Checked the traceback documentation and searched open/closed issues for `SyntaxError`, `ParseError`, and `ExpatError`.
- [x] 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:
```python
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:
```text
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.
"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)"
For the record I encountered the bug using PyPy on this MRE:
```python
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](https://github.com/python/cpython/issues/158166) 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.
I pushed a fix in my fork [here](https://github.com/alexprengere/rich/commit/86f430f0189eea6e208730378593eec3ea9e4125), but I cannot create a PR as I think those are limited to collaborators.
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:
```python
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
```python
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`:
```diff
- 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`](https://github.com/EDfwy1/rich/tree/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!