5

Consider the following code block:

class MyException(Exception):
    __hash__ = None

try:
    raise ExceptionGroup("Foo", [
        MyException("Bar")
    ])
except* Exception:
    pass

The except* should catch any number of exceptions of any kind, thrown together as an ExceptionGroup (or a single exception of any kind if thrown alone, come to that). Instead, an unhandled TypeError occurs during handling of our ExceptionGroup, allegedly at "line -1" of our module:

  + Exception Group Traceback (most recent call last):
  |   File "C:\Users\Josep\AppData\Roaming\JetBrains\PyCharmCE2022.2\scratches\scratch_62.py", line 6, in <module>
  |     raise ExceptionGroup("Foo", [
  | ExceptionGroup: Foo (1 sub-exception)
  +-+---------------- 1 ----------------
    | MyException: Bar
    +------------------------------------

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\Josep\AppData\Roaming\JetBrains\PyCharmCE2022.2\scratches\scratch_62.py", line -1, in <module>
TypeError: unhashable type: 'MyException'

(If we replace the except* Exception with something more specific, say except* ValueError or except* MyException, the same thing happens. If we try to just raise a single MyException and catch it normally with except MyException, that works fine.)

Normal except clauses don't care whether exceptions are hashable. I could not find this quirk of except* documented in PEP-654 or the Python 3.11 release notes. Is this intended behavior, or is it simply a bug in the Python implementation I'm using?

(For those who want to reproduce this behavior, I'm using Python 3.11.0, 64-bit, on Windows.)

J E K
  • 112
  • 5
  • For the curious, the practical relevance of this is when you have an exception with multiple fields, and you try to save yourself some typing by using the `@dataclass` decorator. Non-frozen dataclasses are unhashable by default. – J E K Nov 04 '22 at 08:37
  • I'll fix this, see https://github.com/python/cpython/issues/99181. – Irit Katriel Nov 07 '22 at 07:49
  • 1
    @IritKatriel, thank you very much! You want to make that comment an answer so I can accept it for posterity's sake? I'll answer my own question in 24 hours if you haven't, but I'll feel bad for stealing your rep. – J E K Nov 07 '22 at 09:09

2 Answers2

1

This was reported at https://github.com/python/cpython/issues/99181 and we have a PR to fix it. Should be fixed in 3.11.1.

Irit Katriel
  • 3,534
  • 1
  • 16
  • 18
-2

I wouldn't really call it a bug; it's the programmer fault for breaking the promise Exception class makes (implementing __hash__ method); therefore it's programmer who is responsible for making sure things work as intended after doing this. After all, would you call following snippet a "bug" or "programmer mistake"?

class MyInt(int):
    __hash__ = None

d = {}
d[MyInt(2)] = 2 # TypeError: unhashable type: 'MyInt'

Yours example isn't much different.

matszwecja
  • 6,357
  • 2
  • 10
  • 17
  • 1
    But `Exception` just inherits the default `object.__hash__`. It doesn't make any specific promise of its own that exceptions will be hashable, any more than `object` makes any promise that all objects will be hashable. – user2357112 Nov 04 '22 at 08:10
  • @user2357112 inherited promise is still some kind of promise. – matszwecja Nov 04 '22 at 08:16
  • It doesn't inherit any promise! If it did, `list` would be broken, because `list` inherits from `object`, and lists are unhashable. *Every* unhashable type would be considered broken by that standard. – user2357112 Nov 04 '22 at 08:16
  • Even if we accept this argument (though I'm inclined to @user2357112's take on this), isn't the claim that the exception happened at "line -1" still wrong? – J E K Nov 04 '22 at 08:21
  • @user2357112 Depends on what you consider "broken". The promise is broken, but that does not mean the class itself is broken. It's whoever is writing the class who is reponsible for making sure it works in the context it is supposed to work in. Lists are not supposed to be hashable, so noone calls them not working as `dict` keys "a bug". – matszwecja Nov 04 '22 at 08:22
  • 2
    I think when classes inherit default behavior from `object`, the question of whether that behavior is part of their "contract" comes down to documentation. Is there any official Python documentation that talks about hashing exceptions? – J E K Nov 04 '22 at 08:34
  • Since everything inherits from `object`, the mere existence of unhashable types means breaking the "promise" of `object.__hash__` is acceptable in Python. – MisterMiyagi Nov 04 '22 at 15:11