The documentation for the raise statement with no arguments says
If no expressions are present, raise re-raises the last exception that was active in the
It turns out Python uses a surprising way of building tracebacks. Rather than building the whole stack trace on exception creation (like Java) or when an exception is raised (like I used to think), Python builds up a partial traceback one frame at a time as the exception bubbles up.
Every time an exception bubbles up to a new stack frame, as well as when an exception is raised with the one-argument form of raise (or the two-argument form, on Python 2), the Python bytecode interpreter loop executes PyTraceback_Here to add a new head to the linked list of traceback objects representing the stack trace. (0-argument raise
, and 3-argument raise
on Python 2, skip this step.)
Python maintains a per-thread stack of exceptions (and tracebacks) suspended by except
and finally
blocks that haven't finished executing. 0-argument raise
restores the exception (and traceback) represented by the top entry on this stack, even if the except
or finally
is in a different function.
When f
executes its raise
:
raise Exception
Python builds a traceback corresponding to just that line:
File "foo", line 3, in f
raise Exception
When g
executes 0-argument raise
, this traceback is restored, but no entry is added for the 0-argument raise
line.
Afterwards, as the exception bubbles up through the rest of the stack, entries for the g()
and f()
calls are added to the stack trace, resulting in the final stack trace that gets displayed:
Traceback (most recent call last):
File "foo", line 10, in <module>
f()
File "foo", line 5, in f
g()
File "foo", line 3, in f
raise Exception
Exception
When you raise
without arguments, the interpreter looks for the last exception raised and handled. It then acts the same as if you used raise
with the most recent exception type, value and traceback.
This is stored in the interpreter state for the current thread, and the same information can be retrieved using sys.exc_info(). By 'handled' I mean that an except clause caught the exception. Quoting the try statement documentation:
Before an except clause’s suite is executed, details about the exception are assigned to three variables in the
sys
module:sys.exc_type
receives the object identifying the exception;sys.exc_value
receives the exception’s parameter;sys.exc_traceback
receives a traceback object (see section The standard type hierarchy identifying the point in the program where the exception occurred. These details are also available through thesys.exc_info()
function, which returns a tuple(exc_type, exc_value, exc_traceback)
.
See the implemenation notes in the Python evaluation loop (C code), specifically:
The second bullet was for backwards compatibility: it was (and is) common to have a function that is called when an exception is caught, and to have that function access the caught exception via sys.exc_ZZZ. (Example: traceback.print_exc()).
The traceback reflects how you came to the re-raise accurately. It is the current stack (line 10 calling f()
, line 5 calling g()
) plus the original location of the exception raised: line 3.