classification
Title: Exception chaining accepts exception classes
Type: behavior Stage: resolved
Components: Interpreter Core Versions: Python 3.10, Python 3.9, Python 3.8
process
Status: closed Resolution: not a bug
Dependencies: Superseder:
Assigned To: Nosy List: cool-RR, mark.dickinson, steven.daprano
Priority: normal Keywords:

Created on 2021-01-22 13:52 by cool-RR, last changed 2021-01-23 12:32 by mark.dickinson. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 24303 open mark.dickinson, 2021-01-23 12:32
Messages (5)
msg385499 - (view) Author: Ram Rachum (cool-RR) * Date: 2021-01-22 13:52
I saw this line of code today: https://github.com/hyperledger/sawtooth-sdk-python/commit/c27b962541c9ae68fd1e6dc691ddee883234f112#diff-eb008203eae2160c5e14c42e5fd2eee164709a93bf5136fa79cc256d4e46eaffR92

I was about to tell this guy that his code is bad since the part after the `from` should be a specific exception, not just an exception class. I thought I should double-check myself first. Lo and behold: 

```
$ cat x.py
def f():
    raise TypeError

try:
    f()
except TypeError:
    raise ValueError from TypeError

$ python x.py
TypeError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "x.py", line 7, in <module>
    raise ValueError from TypeError
ValueError
```

The line doesn't fail, but it silently trims away the stacktrace of the previous exception. (Normally the stacktrace would include what's inside the `f` function.)

I'm not sure whether this is intended behavior or not. The documentation does say "the second expression must be another exception class or instance" but (a) it's not clear what the use case is when it's a class and (b) I doubt that the person who wrote the code above, and any other person who writes such code, would be aware that their traceback got snipped until it's too late and they're stuck with a bug report with incomplete information.
msg385514 - (view) Author: Steven D'Aprano (steven.daprano) * (Python committer) Date: 2021-01-22 22:29
It is intended behaviour. `raise ... from` is a general mechanism that you can call anywhere, it is not just limited to raising from the previous exception. It is designed for explicitly setting the chained exception to some arbitrary exception. See the PEP:

https://www.python.org/dev/peps/pep-3134/#explicit-exception-chaining

>>> raise ValueError('bad value') from IndexError('bad index')
IndexError: bad index

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: bad value


If you wish to chain from the current exception, you have to chain from the current exception, and not create a new one.

This is not an interpreter bug, it's working fine. Your first instinct was correct: your colleague had written "bad code" and his intention was probably to raise from the exception he had just caught. 

Or just don't do anything. A `raise exception` inside an except block will be automatically chained with the current exception unless you explicitly disable it with `raise exception from None`.
msg385525 - (view) Author: Ram Rachum (cool-RR) * Date: 2021-01-23 07:06
People barely know how to use the right form of this feature. Providing them with a wrong form that's confusingly similar to the right form and fails silently is an unfortunate choice. 

"Or just don't do anything. A `raise exception` inside an except block will be automatically chained with the current exception [...]"

With the wrong message, yes.
msg385530 - (view) Author: Steven D'Aprano (steven.daprano) * (Python committer) Date: 2021-01-23 12:05
How do you "the wrong message" to implicitly chain exceptions rather 
than explicitly?

The difference between:

     try:
         len(1)
     except TypeError as e:
         raise ValueError(msg) from e

and

     try:
         len(1)
     except TypeError as e:
       	 raise ValueError(msg)

is that the first traceback says:

"The above exception was the direct cause of the following exception"

and the second says:

"During handling of the above exception, another exception occurred"

Both messages are correct, but if the difference beween the two matters 
to you, feel free to use whichever form you prefer.
msg385531 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2021-01-23 12:08
Ram: I think you're conflating two separate things, here:

(1) The ability to use an exception *class* instead of an exception *instance* in the "from" clause - that is, the ability to do "raise ValueError from TypeError" in place of "raise ValueError from TypeError()"

(2) The lack of a traceback from the local exception-handling context when doing raise from.

The two are independent: you'll see the same lack of traceback that you described if you do "raise ValueError from TypeError()" instead of "raise ValueError from TypeError".

Both behaviours are by design (as Steven already pointed out for (2)). However, on point (1), there may be a documentation bug here. The reference manual, under https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement, says:

> The from clause is used for exception chaining: if given, the second expression must be another exception class or instance, which will then be attached to the raised exception as the __cause__ attribute (which is writable).

However, this description appears not to match the implementation. In the case that the second expression is an exception class, it's *not* attached to the raised exception as the __cause__ attribute. Instead, the exception class is first instantiated, and then the resulting exception *instance* is attached to the raised exception as the __cause__ attribute.

The corresponding part of the implementation is here: https://github.com/python/cpython/blob/b745a6143ae79efe00aa46affe5ea31a06b0b532/Python/ceval.c#L4758-L4763

Demonstration:

    >>> try:
    ...     raise ZeroDivisionError() from RuntimeError
    ... except Exception as e:
    ...     exc = e
    ... 
    >>> exc.__cause__
    RuntimeError()
    >>> exc.__cause__ is RuntimeError  # reference manual would suggest this is True
    False
    >>> isinstance(exc.__cause__, RuntimeError)  # actual behaviour
    True
History
Date User Action Args
2021-01-23 12:32:40mark.dickinsonsetpull_requests: + pull_request23124
2021-01-23 12:08:30mark.dickinsonsetnosy: + mark.dickinson
messages: + msg385531
2021-01-23 12:05:10steven.dapranosetmessages: + msg385530
2021-01-23 07:06:29cool-RRsetmessages: + msg385525
2021-01-22 22:29:37steven.dapranosetstatus: open -> closed

nosy: + steven.daprano
messages: + msg385514

resolution: not a bug
stage: resolved
2021-01-22 13:52:41cool-RRcreate