classification
Title: Re-raising exceptions from an expression
Type: enhancement Stage: resolved
Components: Interpreter Core Versions: Python 3.4, Python 3.3
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: Nosy List: Arfrever, Tyler.Crompton, ethan.furman, georg.brandl, ncoghlan, python-dev
Priority: normal Keywords: patch

Created on 2012-06-27 18:27 by Tyler.Crompton, last changed 2012-12-09 06:24 by ncoghlan. This issue is now closed.

Files
File name Uploaded Description Edit
getch.py Tyler.Crompton, 2012-06-27 18:27 Source that raises syntax error (same thing as in comment)
raise_from_doc_update.diff ethan.furman, 2012-06-27 22:20 review
raise_from_doc_update_v2.diff ethan.furman, 2012-06-28 16:15 review
Messages (21)
msg164184 - (view) Author: Tyler Crompton (Tyler.Crompton) Date: 2012-06-27 18:27
As you know, a caught exception can be re-raised with a simple `raise` statement. Plain and simple. However, one cannot re-raise an error with this new `"from" expression` clause.

For example:

    def getch(prompt=''):
        '''Get and return a character (similar to `input()`).'''

        print(prompt, end='')
        try:
            return windows_module.getch()
        except NameError:
            try:
                fallback_module.getch()
            except Exception:
                raise from None

Output:

      File "getch.py", line 11
        raise from None
                 ^
    SyntaxError: invalid syntax

A quick look at the documentation about [raise](http://docs.python.org/dev/reference/simple_stmts.html#the-raise-statement) confirms that this is the intended behavior. In my opinion, one should be able to still re-raise from an expression.
msg164186 - (view) Author: Tyler Crompton (Tyler.Crompton) Date: 2012-06-27 18:36
Relevent PEP: http://www.python.org/dev/peps/pep-0409/
msg164198 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2012-06-27 20:33
I agree that "raise from None" would be a nice enhancement.

I don't see it going into 3.3 since we've hit feature freeze.

Nick, do we need another PEP, or just consensus on pydev?  (If consensus, I can bring it up there after 3.3.0final is released.)
msg164204 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-06-27 21:28
The from clause is intended for replacing previous exceptions with *new*
exceptions, not editing the attributes of existing ones which may already
have a different __cause__ set. So - 1 from me, even for 3.4. A patch to
the docs explaining that this is not supported syntactically because it
risks losing debugging data would be fine, though.
msg164205 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2012-06-27 21:36
Okay, I see your point.  It's also not difficult to work around if you really want to toss the extra info:

        except NameError:
            try:
                fallback_module.getch()
            except Exception as exc:
                raise exc from None

A total of three more words to get the desired behavior (and small ones at that).

I'll work on a doc patch.
msg164206 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2012-06-27 22:20
Patch attached.  It basically says:

8<--------------------------------------------------------------------
Note:  Because using :keyword:`from` can throw away valuable debugging information, its use with a bare :keyword:`raise` is not supported. If you are trying to do this:

    try:
        some_module.not_here()
    except NameError:
        try:
            backup_module.not_here()
        except NameError:
            raise from None     # suppress context in NameError

do this instead:

    try:
        some_module.not_here()
    except NameError:
        try:
            backup_module.not_here()
        except NameError as exc:
            raise exc from None     # suppress context in NameError
8<--------------------------------------------------------------------
msg164208 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2012-06-27 22:27
Nick Coghlan wrote:
> The from clause is intended for replacing previous exceptions with *new*
> exceptions, not editing the attributes of existing ones which may already
> have a different __cause__ set.

Huh.  While I agree with the doc patch solution, I think the most common 
use of 'from' will be 'raise SomeException from None' or, as the patch 
suggests, 'raise exc from None' (exc being an already existing exception).

Any examples of when somebody might do:

try:
    do_something()
except NameError:
    raise NewError from SomeOtherError

?

I am unsure of the advantages in replacing NameError in the above stack 
trace with SomeOtherError instead... although NameError would still be 
there in __context__...

Still, any examples?

~Ethan~
msg164214 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-06-28 00:11
The purpose of the from clause in general is to change the exception *type* (for example, from KeyError -> AttributeError or vice-versa), or to add additional information without losing access to any previous information (like the original traceback). This is covered in PEP 3134.

In some cases, the appropriate way to convey the information is to copy relevant details to the new exception, leave the context set, and suppress display of that context by default. In other cases, the developer may want to display the chained exception explicitly, because it *isn't* part of the current exception handling context (e.g. this is quite likely in an event loop scheduler that passes exception information between events - the __cause__ chain would track the *event stack* rather than the Python call stack).

Thus, in the recommended use for the "raise X from Y" syntax, you're always setting __cause__ on a *new* exception, so you know you're not overwriting a preexisting __cause__ and thus there's no risk of losing information that might be relevant for debugging the failure.

When you use "raise X from Y" directly on a *pre-existing* exception (however you managed to get hold of it), there's a chance that __cause__ is already set. If you blindly overwrite it, then you've deleted part of the exception chain that PEP 3134 is designed to preserve.

This is also why Guido and I were so adamant that just setting __context__ to None was not an acceptable solution for PEP 409 - the whole point of PEP 3144 is to make it difficult to accidentally lose the full details of the traceback even if some of the exception handlers in the stack are written by developers that don't have much experience in debugging other people's code.
msg164260 - (view) Author: Tyler Crompton (Tyler.Crompton) Date: 2012-06-28 14:43
I'm in a little over my head as I can't conceptualize __cause__, so I may be looking over things.

First, you, Ethan, said the following:

>It's also not difficult to work around if you really want to toss the extra info:
>
>        except NameError:
>            try:
>                fallback_module.getch()
>            except Exception as exc:
>                raise exc from None
>
>A total of three more words to get the desired behavior (and small ones at that).

Counter-argument: if it's just three words, then why was the shorthand without the from clause implemented in the first place?

My use case was primarily based on the idea that the unavailability of the windows module (from the example) is irrelevant information to, say, Unix users. When an exception is raised, the user shouldn't have to see any Windows-related exceptions (that is if there is an alternate solution).

One could fix this with a little bit of refactoring, though:

    import sys as _sys

    def getch(prompt=''):
        '''Get and return a character (similar to `input()`).'''

        print(prompt, end='')
        if 'windows_module' in _sys.modules:
            return windows_module.getch()
        else:
            try:
                return fallback_module.getch()
            except Exception:
                raise from None

But it's EAFP. Heck, one could even do the following:

    def getch(prompt=''):
        '''Get and return a character (similar to `input()`).'''

        print(prompt, end='')
        try:
            return windows_module.getch()
        except NameError:
            pass

        try:
            return fallback_module.getch()
        except Exception:
            raise

But that's not really ideal. I've played around with the traceback module a little and (very) briefly played with the exceptions themselves. Is there not an easier way to suppress a portion of an exception? Like I said, such information is irrelevant to non-Windows users.
msg164268 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-06-28 15:59
If you don't want the exception context set *at all*, just use a pass statement to get out of the exception before trying the fallback.

    try:
        return windows_module.getch()
    except NameError:
        pass # No exception chaining
    fallback_module.getch()
msg164269 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-06-28 16:02
Correction, your try block is overbroad and will suppress errors in the getch implementation. This is better:

    try:
        _getch = windows_module.getch
    except NameError:
        _ getch = fallback_module.getch
    _getch()

So I'm sticking with my perspective that wanting to do this is a sign of something else being wrong with the exception handling setup.
msg164271 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2012-06-28 16:08
Tyler Crompton wrote:
> I'm in a little over my head as I can't conceptualize __cause__, so I may be looking over things.
> 
> First, you, Ethan, said the following:
> 
>> It's also not difficult to work around if you really want to toss the extra info:
>>
>>        except NameError:
>>            try:
>>                fallback_module.getch()
>>            except Exception as exc:
>>                raise exc from None
>>
>> A total of three more words to get the desired behavior (and small ones at that).
> 
> Counter-argument: if it's just three words, then why was the shorthand without the from clause implemented in the first place?

I'm not sure I understand the question -- do you mean why can we do 
'raise' by itself to re-raise an exception?  'from' is new, and was 
added in Py3k (see below).  'raise', as a shortcut, is there to allow 
clean-up (or whatever) in the except clause before re-raising the same 
exception.

In 3.0 exceptions were enhanced to include a link to previous 
exceptions.  So if you are handling exception A and exception B occurs, 
exception B will be raised and will have a link to A.  That link is kept 
in __context__.  This complete chain will then be printed if the last 
exception raised is uncaught.

However, there are times when you may want to add more exception 
information yourself, so we have the `from` clause, which store the 
extra exception in __cause__.

And, there are times when you are changing from one exception to another 
and do not want the previous one displayed -- so we now have 'from None' 
(which sets __suppress_context__ to True).  So if some underlying 
function raises ValueError, but you want to transform that to an 
XyzError, your can do:

     try:
         some_function()
     except ValueError:
         raise XyzError from None

and then, if the exception is uncaught and printed, only the XyzError 
will be displayed (barring custom print handlers).

> My use case was primarily based on the idea that the unavailability of the windows module (from the example) is irrelevant information to, say, Unix users. When an exception is raised, the user shouldn't have to see any Windows-related exceptions (that is if there is an alternate solution).

So you are using the absence of the Windows based module as evidence 
that you are not running on Windows... but what if you are on Windows 
and there is some other problem with that module?

The usual way to code for possible different modules is:

     try:
         import windows_module as utils  # or whatever name
     except ImportError:
         import fallback_module as utils
msg164272 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2012-06-28 16:15
Removed sample code from doc patch as it was not robust, nor recommended practice.  New patch is effectively:

 
Note:  Because using :keyword:`from` can throw away valuable debugging information, its use with a bare :keyword:`raise` is not supported.
msg166428 - (view) Author: Tyler Crompton (Tyler.Crompton) Date: 2012-07-25 21:46
As for the losing valuable debug information, much worse can be done:

import sys

try:
	x
except NameError:
	print('An error occurred.', file=sys.stderr)
	sys.exit(1)

This is obviously not how one should handle errors in development, but it's allowed. As Ethan pointed out, the initial proposal can be recreated by just adding three words which is obviously also allowed.

Nick, I'm not saying you're opinions are wrong, I just wanted to point out how easy it is to throw away valuable information. It's almost futile.
msg168704 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2012-08-20 22:00
Any problems with the current doc patch?  If not, can it be applied before RC1?
msg170339 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2012-09-11 18:16
Can we also get this committed before 3.3.0 final?
msg177157 - (view) Author: Roundup Robot (python-dev) Date: 2012-12-08 12:24
New changeset 8ba3c975775b by Nick Coghlan in branch '3.3':
Issue #15209: Clarify exception chaining description
http://hg.python.org/cpython/rev/8ba3c975775b

New changeset 5854101552c2 by Nick Coghlan in branch 'default':
Merge from 3.3 (Issue #15209)
http://hg.python.org/cpython/rev/5854101552c2
msg177158 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-12-08 12:27
I rewrote the relevant section of the module docs (since they were a bit murky in other ways as well).

Since I didn't answer the question earlier, the main reason a bare raise is permitted is because it's designed to be used to a bare except clause (e.g. when rolling back a database transaction as a result of an error). While you could achieve the same thing now with "except BaseException", the requirement for all exceptions to inherit from BaseException is relatively recent - back in the days of string exceptions there was simply no way to catch arbitrary exceptions *and* give them a name.
msg177163 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2012-12-08 16:59
There is one typo and one error in the first paragraph of the patch:

> When raising a new exception (rather than
> using to bare ``raise`` to re-raise the
         ^ should be an 'a'

> exception currently being handled), the
> implicit exception chain can be made explicit
> by using :keyword:`from` with :keyword:`raise`.
> The single argument to :keyword:`from` must be
> an exception or ``None``. It will be set as
> :attr:`__cause__` on the raised exception.

> Setting :attr:`__cause__` also implicitly sets
> the :attr:`__suppress_context__` attribute to ``True``.

The last sentence is incorrect -- __suppress_context__ is only set to True if __cause__ is set to None; if __cause__ is set to any other exception __suppress_context__ remains False and the new exception chain will be printed:

>>> try:
...   raise ValueError
... except:
...   raise NameError from KeyError
...
KeyError

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

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
NameError

This is easily fixed by adding 'to ``None``':

> Setting :attr:`__cause__` to ``None`` also implicitly sets
> the :attr:`__suppress_context__` attribute to ``True``.
msg177196 - (view) Author: Roundup Robot (python-dev) Date: 2012-12-09 06:22
New changeset 3b67247f0bbb by Nick Coghlan in branch '3.3':
Issue #15209: Fix typo and some additional wording tweaks
http://hg.python.org/cpython/rev/3b67247f0bbb

New changeset 04eb89e078b5 by Nick Coghlan in branch 'default':
Merge from 3.3 (issue #15209)
http://hg.python.org/cpython/rev/04eb89e078b5
msg177197 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2012-12-09 06:24
On Sun, Dec 9, 2012 at 2:59 AM, Ethan Furman <report@bugs.python.org> wrote:

>
> Ethan Furman added the comment:
>
> There is one typo and one error in the first paragraph of the patch:
>
> > When raising a new exception (rather than
> > using to bare ``raise`` to re-raise the
>          ^ should be an 'a'
>

Fixed.

> > Setting :attr:`__cause__` also implicitly sets
> > the :attr:`__suppress_context__` attribute to ``True``.
>
> The last sentence is incorrect -- __suppress_context__ is only set to True
> if __cause__ is set to None; if __cause__ is set to any other exception
> __suppress_context__ remains False and the new exception chain will be
> printed:
>
> >>> try:
> ...   raise ValueError
> ... except:
> ...   raise NameError from KeyError
> ...
> KeyError
>
> The above exception was the direct cause of the following exception:
>
> Traceback (most recent call last):
>   File "<stdin>", line 4, in <module>
> NameError
>

Not true: __suppress_context__ is always set as a side effect of setting
__cause__ (it's built into the setter for the __cause__ descriptor). What
you're seeing in the traceback above is the explicit cause, not the
implicit context.

>>> e = Exception()
>>> e.__cause__ = Exception()
>>> e.__suppress_context__
True

The only mechanism we offer to suppress an explicit __cause__ is setting
__cause__ to None.

Cheers,
Nick.
History
Date User Action Args
2012-12-09 06:24:12ncoghlansetmessages: + msg177197
2012-12-09 06:22:30python-devsetmessages: + msg177196
2012-12-08 16:59:16ethan.furmansetmessages: + msg177163
2012-12-08 12:27:30ncoghlansetstatus: open -> closed
resolution: fixed
messages: + msg177158

stage: resolved
2012-12-08 12:24:42python-devsetnosy: + python-dev
messages: + msg177157
2012-09-11 18:22:14ezio.melottisetnosy: + georg.brandl
2012-09-11 18:16:56ethan.furmansetmessages: + msg170339
2012-08-20 22:00:32ethan.furmansetmessages: + msg168704
2012-07-25 21:46:22Tyler.Cromptonsetmessages: + msg166428
2012-06-28 16:15:21ethan.furmansetfiles: + raise_from_doc_update_v2.diff

messages: + msg164272
2012-06-28 16:08:15ethan.furmansetmessages: + msg164271
2012-06-28 16:02:12ncoghlansetmessages: + msg164269
2012-06-28 15:59:34ncoghlansetmessages: + msg164268
2012-06-28 14:43:56Tyler.Cromptonsetmessages: + msg164260
2012-06-28 00:11:57ncoghlansetmessages: + msg164214
2012-06-27 22:27:45ethan.furmansetmessages: + msg164208
2012-06-27 22:20:12ethan.furmansetfiles: + raise_from_doc_update.diff
keywords: + patch
messages: + msg164206
2012-06-27 21:36:32ethan.furmansetmessages: + msg164205
2012-06-27 21:28:17ncoghlansetmessages: + msg164204
2012-06-27 21:19:36Arfreversetnosy: + Arfrever
2012-06-27 20:33:40ethan.furmansetmessages: + msg164198
2012-06-27 18:57:55Tyler.Cromptonsetnosy: + ncoghlan, ethan.furman
2012-06-27 18:36:32Tyler.Cromptonsetmessages: + msg164186
2012-06-27 18:27:45Tyler.Cromptoncreate