classification
Title: traceback module can't format/print unhashable exceptions
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.7, Python 3.6
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: Nosy List: Chris Barth, Jelle Zijlstra, Trundle, berker.peksag, serhiy.storchaka, terry.reedy, zaneb
Priority: normal Keywords: patch

Created on 2016-11-03 21:22 by Trundle, last changed 2017-10-17 22:17 by serhiy.storchaka. This issue is now closed.

Files
File name Uploaded Description Edit
unhashable_exceptions.diff Trundle, 2016-11-03 21:22 Patch against default branch review
issue28603-list.patch Jelle Zijlstra, 2016-12-13 08:01 review
issue28603-ignore.patch Jelle Zijlstra, 2016-12-13 08:01 review
issue28603-list.patch Jelle Zijlstra, 2016-12-13 08:01 review
hashex.py zaneb, 2017-10-16 22:44
Pull Requests
URL Status Linked Edit
PR 4014 merged zaneb, 2017-10-16 22:28
PR 4024 merged python-dev, 2017-10-17 21:30
Messages (11)
msg280022 - (view) Author: Andreas Stührk (Trundle) * Date: 2016-11-03 21:22
The traceback module tries to handle loops caused by an exception's __cause__ or __context__ attributes when printing tracebacks. To do so, it adds already seen exceptions to a set. Unfortunately, it doesn't handle unhashable exceptions:

>>> class E(Exception): __hash__ = None
...
>>> traceback.print_exception(E, E(), None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.5/traceback.py", line 100, in print_exception
    type(value), value, tb, limit=limit).format(chain=chain):
  File "/usr/lib/python3.5/traceback.py", line 439, in __init__
    _seen.add(exc_value)
TypeError: unhashable type: 'E'

CPython's internal exception printing pretty much does the same, except it ignores any exception while operating on the seen set (see https://hg.python.org/cpython/file/8ee4ed577c03/Python/pythonrun.c#l813 ff).

Attached is a patch that makes the traceback module ignore TypeErrors while operating on the seen set. It also adds a (minimal) test.
msg280233 - (view) Author: Berker Peksag (berker.peksag) * (Python committer) Date: 2016-11-07 20:31
Thanks for the patch. Out of curiosity, how did you get an unhashable exception? Is there a way to reproduce this without creating a custom exception and set __hash__ to None? In other words, what's your use case?
msg280237 - (view) Author: Andreas Stührk (Trundle) * Date: 2016-11-07 20:40
It was reported as bug to a project that uses the traceback module (https://github.com/bpython/bpython/issues/651). Parsley (https://pypi.python.org/pypi/Parsley) is at least one library that uses unhashable exceptions, although I guess it's by accident: it defines __eq__, but not __hash__ and that makes the exception unhashable under Python 3.
msg283081 - (view) Author: Jelle Zijlstra (Jelle Zijlstra) * Date: 2016-12-13 08:01
I ran into this bug through Thrift-generated exception classes (also reported there as https://issues.apache.org/jira/browse/THRIFT-4002).

I've added a few potential solutions:
- issue28603-listset.patch turns the seen set into a list if hashing fails. However, this adds a lot of complexity, especially in C, and because seen is changed halfway through the recursion, we may end up showing an exception twice.
- issue28603-list.patch uses a list instead of a set for seen. This is theoretically slower, but in practice it seems unlikely that exception __cause__ and __context__ would nest deep enough for this to be an issue.
- issue28603-ignore.patch takes a similar approach to Trundle's patch and just gives up when the value is not hashable. This means we lose cause/context information for these exceptions.

I prefer issue28603-list.patch.
msg283095 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2016-12-13 11:15
There is no issue28603-listset.patch.
msg296034 - (view) Author: Chris Barth (Chris Barth) Date: 2017-06-14 20:30
There are now several patches for this problem, which also affects me. What are the next steps to get this resolved?
msg304485 - (view) Author: Zane Bitter (zaneb) * Date: 2017-10-16 22:44
I also encountered this issue, and I'd like to further note that it is extraordinarily difficult to debug without access to stderr. If your logging facility uses the traceback module then it will be unable to record both the original (unhashable) exception, and the TypeError that it triggers (since the original exception is its __context__). The effect is that execution of a thread appears to simply stop without rhyme or reason (in fact it is still executing, but that fact is not apparent from the logs). I'm attaching a short reproducer that demonstrates this effect.

I believe the trade-offs inherent in the attached patches (either stopping the chain at an unhashable exception, or using a less-efficient data structure) can be avoided by comparing for identity rather than equality. The purpose of the check is to avoid an infinite loop; whether the objects compare as equal is something that is up to the author of the class, and has no relevance here except insofar as that objects ought to compare equal to themselves.

I submitted a pull request (#4014) with an implementation of that.
msg304534 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2017-10-17 21:22
As I understand it, the patch amounts to ignoring any custom __eq__ and __hash__ on an Exception class when printing tracebacks and, in effect, using the default id-based versions inherited from object, as is being assumed.  This seems right to me in this context.
msg304535 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2017-10-17 21:29
New changeset de86073a761cd3539aaca6f886a1f55effc0d9da by Serhiy Storchaka (Zane Bitter) in branch 'master':
bpo-28603: Fix formatting tracebacks for unhashable exceptions (#4014)
https://github.com/python/cpython/commit/de86073a761cd3539aaca6f886a1f55effc0d9da
msg304538 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2017-10-17 22:14
New changeset 2712247ec94dcc12cf9abeec78f18f54fcc3357a by Serhiy Storchaka (Miss Islington (bot)) in branch '3.6':
[3.6] bpo-28603: Fix formatting tracebacks for unhashable exceptions (GH-4014) (#4024)
https://github.com/python/cpython/commit/2712247ec94dcc12cf9abeec78f18f54fcc3357a
msg304539 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2017-10-17 22:17
Thank you for your contribution Zane.
History
Date User Action Args
2017-10-17 22:17:00serhiy.storchakasetstatus: open -> closed
versions: - Python 3.5
messages: + msg304539

resolution: fixed
stage: patch review -> resolved
2017-10-17 22:14:21serhiy.storchakasetmessages: + msg304538
2017-10-17 21:30:48python-devsetpull_requests: + pull_request3999
2017-10-17 21:29:42serhiy.storchakasetmessages: + msg304535
2017-10-17 21:22:20terry.reedysetnosy: + terry.reedy
messages: + msg304534
2017-10-16 22:44:28zanebsetfiles: + hashex.py
nosy: + zaneb
messages: + msg304485

2017-10-16 22:28:29zanebsetpull_requests: + pull_request3988
2017-06-14 20:30:43Chris Barthsetmessages: + msg296034
2017-06-14 20:29:04Chris Barthsetnosy: + Chris Barth
2016-12-13 11:15:56serhiy.storchakasetnosy: + serhiy.storchaka
messages: + msg283095
2016-12-13 08:01:49Jelle Zijlstrasetfiles: + issue28603-list.patch
2016-12-13 08:01:37Jelle Zijlstrasetfiles: + issue28603-ignore.patch
2016-12-13 08:01:16Jelle Zijlstrasetfiles: + issue28603-list.patch
nosy: + Jelle Zijlstra
messages: + msg283081

2016-11-07 20:40:32Trundlesetmessages: + msg280237
2016-11-07 20:31:27berker.peksagsetversions: + Python 3.5, Python 3.6, Python 3.7
nosy: + berker.peksag

messages: + msg280233

type: behavior
stage: patch review
2016-11-03 21:22:14Trundlecreate