Skip to content

gh-149313: Preserve exception notes/cause in dict/set unhashable type… - #149505

Open
anujbharambe wants to merge 2 commits into
python:mainfrom
anujbharambe:fix-unhashable-exception-chaining
Open

anujbharambe wants to merge 2 commits into
python:mainfrom
anujbharambe:fix-unhashable-exception-chaining

Conversation

@anujbharambe

@anujbharambe anujbharambe commented May 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Chain the original TypeError as __cause__ and __context__ on the new exception in dict_unhashable_type() and set_unhashable_type(). This preserves __notes__, __traceback__, and any existing cause chain that were previously discarded when the improved error message was created.

Fixes #149313
Some parts of this PR were written with the assistance of Claude (AI).

@StanFromIreland

Copy link
Copy Markdown
Member

Updating to fix some errors we introduced on the main branch.

@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open for 90 days with no activity.

localzet commented Nov 6, 2026

Copy link
Copy Markdown

I looked at the duplicate-traceback concern raised in gh-149313 and tested a conditional alternative in my fork: localzet#2.

The behavior is:

  • If the original TypeError has a traceback, cause, or context, chain it as the replacement's __cause__/__context__, preserving the full diagnostic chain.
  • Otherwise, if the original error only has __notes__ (for example, a C-level tuple hashing failure carrying an item-index note), copy those notes to the improved outer TypeError without printing the inner error twice.
  • If neither applies, retain the current concise error message.

This addresses the traceback regression without always adding a redundant exception block for builtin unhashable objects. The experiment includes dict/set regression tests and the tuple-hash change from gh-161433 for integration testing; it is not a standalone upstream patch as-is.

The debug build, test_tuple, test_dict, test_set, test_traceback, and lint passed: https://github.com/localzet/cpython/actions/runs/42290822325.

I don't intend to open a competing upstream PR while this one is open. Would this conditional behavior fit the direction you want for gh-149313? I can help adapt the patch if useful.

@WakaTaira

Copy link
Copy Markdown

Thanks, @localzet. The notes-only path improves on what I suggested in gh-149313: {([1],): 1} should keep the single message and still show the item note, without printing the inner error twice.

One case to check is using __context__ as a reason to chain. A C-level TypeError picks up the exception being handled as its __context__, even when there is nothing else to preserve:

try:
    1/0
except ZeroDivisionError:
    try:
        hash(([],))
    except TypeError as e:
        print(repr(e.__context__))  # ZeroDivisionError('division by zero')

From reading the diff, {([1],): 1} inside any except block would therefore take the chaining path and show the duplicated message again. The replacement TypeError already gets the same context from PyErr_Format(), and a Python-level __hash__ still has a traceback, so would checking only the traceback and cause be enough? A regression test for this case would also help.

I haven't built your branch, so please correct me if I've misread it.

@anujbharambe, are you still planning to continue this PR? If not, I'd be happy to prepare one with the conditional behavior.

@WakaTaira

WakaTaira commented Nov 7, 2026 •

Copy link
Copy Markdown

@anujbharambe, @localzet: I've opened #161536 with the conditional behavior. I asked above whether you were continuing here and didn't wait long for an answer, so I'm sorry about that. This was my first time filing an issue on a project this large, and I wanted to see this change through to a PR myself.

It builds on what you both did here and in the fork experiment, and I'm happy to close it if you'd rather continue with this PR.

localzet commented Nov 7, 2026

Copy link
Copy Markdown

Thanks for catching that, @WakaTaira — you're right. An ambient __context__ from an enclosing except block should not, by itself, trigger explicit chaining. I removed it from the condition in the fork experiment and added dict/set regression tests for tuple hashing inside except blocks (localzet/cpython@ced6e891; CI is running).

Please keep #161536 open. I'm happy for your PR to be the upstream implementation: it incorporates the conditional behavior, addresses the context issue, and puts the shared logic in one helper. My fork PR #2 is only a proof of concept and I won't submit a competing upstream PR. Thanks for taking this forward, and for crediting the earlier experiments.

@WakaTaira

Copy link
Copy Markdown

@localzet Thank you so much. Honestly, I was a little nervous when I saw your PR, so your kind response really means a lot to me. Thanks for adding me to Misc/ACKS and for supporting #161536. I really appreciate it!

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

Labels

awaiting review stale Stale PR or inactive for long period of time.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exception notes discarded from TypeError within __hash__()

5 participants