Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Fixed

- Fixed `__notes__` from the outermost exception being shown on every exception in a chain; notes are now rendered only on the specific exception they were added to https://github.com/Textualize/rich/issues/3960

## [14.3.3] - 2026-02-19

### Fixed
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ The following people have contributed to the development of Rich:
- [Nicolas Simonds](https://github.com/0xDEC0DE)
- [Aaron Stephens](https://github.com/aaronst)
- [Karolina Surma](https://github.com/befeleme)
- [Kanchan Thapa](https://github.com/kanchanthapa)
- [Gabriele N. Tornetta](https://github.com/p403n1x87)
- [Nils Vu](https://github.com/nilsvu)
- [Arian Mollik Wasi](https://github.com/wasi-master)
Expand Down
8 changes: 6 additions & 2 deletions rich/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,8 +467,6 @@ def extract(

from rich import _IMPORT_CWD

notes: List[str] = getattr(exc_value, "__notes__", None) or []

grouped_exceptions: Set[BaseException] = (
set() if _visited_exceptions is None else _visited_exceptions
)
Expand All @@ -481,6 +479,12 @@ def safe_str(_object: Any) -> str:
return "<exception str() failed>"

while True:
# Read __notes__ from the current exception in the chain, not the
# outermost one. Fixes https://github.com/Textualize/rich/issues/3960
# where notes from the outer exception leaked onto every inner
# exception via the shared list reference.
notes: List[str] = getattr(exc_value, "__notes__", None) or []

stack = Stack(
exc_type=safe_str(exc_type.__name__),
exc_value=safe_str(exc_value),
Expand Down
46 changes: 46 additions & 0 deletions tests/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,52 @@ def test_notes() -> None:
assert traceback.trace.stacks[0].notes == ["Hello", "World"]


@pytest.mark.skipif(
sys.version_info.minor < 11, reason="Not supported before Python 3.11"
)
def test_notes_isolated_per_exception_in_chain() -> None:
"""Regression test for https://github.com/Textualize/rich/issues/3960

__notes__ added to one exception in a chain must not leak onto other
exceptions in the chain. Matches CPython's traceback module behavior.
"""
# __cause__ chain (explicit ``raise ... from ...``)
try:
try:
raise ValueError("inner")
except ValueError as exc:
raise RuntimeError("outer") from exc
except RuntimeError as err:
err.add_note("note-on-outer-only")
traceback = Traceback()

stacks = traceback.trace.stacks
# stacks[0] is the outermost (RuntimeError), stacks[1] is the cause
# (ValueError). The note must appear only on the outer stack.
assert stacks[0].exc_type == "RuntimeError"
assert stacks[0].notes == ["note-on-outer-only"]
assert stacks[1].exc_type == "ValueError"
assert stacks[1].notes == []

# __context__ chain (implicit chaining) with a note only on the inner
# exception in source order (the context). The outer should have no notes.
try:
try:
err = ValueError("context-inner")
err.add_note("note-on-context-only")
raise err
except ValueError:
raise RuntimeError("context-outer")
except RuntimeError:
traceback = Traceback()

stacks = traceback.trace.stacks
assert stacks[0].exc_type == "RuntimeError"
assert stacks[0].notes == []
assert stacks[1].exc_type == "ValueError"
assert stacks[1].notes == ["note-on-context-only"]


def test_recursive_exception() -> None:
"""Regression test for https://github.com/Textualize/rich/issues/3708

Expand Down