sys.exc_type etc.

Guido van Rossum (Guido.van.Rossum@cwi.nl)
Tue, 03 Mar 1992 13:07:37 +0100

Looks like Steve and Tim have uncovered another murky area in Python...

To summarize the story so far for our new readers, and for those who
may not yet have acquired the Python fluency needed to read long
listings of Python code exemplifying a particular point:

(1) Tim wants to catch an exception, print some message, and then
re-raise the same exception, without ever caring *which* exception he
caught, as long as he re-raises the same one that he caught. He
discovers that this is possible using try...finally and a Boolean
variable.

(2) Steven replies that there used to be undocumented variables
sys.exc_type and sys.exc_value identifying the most recent exception,
which could be used to re-raise the exception, but he can't find them
in his current Python version. He also remarks that this is cleaner
than using try...finally.

(3) Tim discovers that sys.exc_type etc. still exist, but only get set
under obscure circumstances involving at least one try...except block
that does not catch the exception, and one stack unwind. He then
expresses the wish that sys.exc_type etc. be set each time an
exception occurs.

Now my comments:

First, I think that Tim's original solution to his problem is
preferable over Steven's version, because Tim's preserves the
traceback: in Steven's code the traceback printed by the interpreter
ends at the "raise sys.exc_type, sys.exc_value" statement in his
except clause, which isn't very helpful for finding the cause of the
problem. Using "finally", on the other hand, preserves the original
traceback (which is, incidentally, saved as sys.exc_traceback in some
versions of the interpreter).

Next, an explanation of how sys.exc_type etc. work is in order. I
added them as an afterthought when I found out that if an exception
handler handles several types of exceptions at a time, it often needs
to know which exception occurred (if only to print the right message).
I played with the thought of changing the syntax so the handler could
specify a variable in which to receive the exception type (just like
it can already do for the exception's "parameter" or "associated
value"), but quickly realized that the feature is most needed for
default "except:" clauses, where adding new syntax was impossible to
do to in an elegant, yet backward-compatible way! At that time,
changing the syntax in an incompatible way would already be rather
painful, so I opted for some secret variables. (And forgot to
document them properly -- sorry about that!)

Now, the choice of *when* sys.exc_type is set must be explained. (I
won't mention sys.exc_value again, since it is set whenever
sys.exc_type is set.) Tim thinks he wants them set each time an
exception occurs, but there's a problem with that. When searching for
an exception handler, the interpreter may execute a lot of user code:
evaluating the expressions in except clause headers, but also finally
clauses of inner try statements. All this code may itself raise
exceptions, as long as it catches them -- so if sys.exc_type were set
each time an exception occured, an outer exception handle might see
the wrong exception type. For example:

try:
try:
1/0
finally:
try:
raise EOFError
except:
pass
except:
print sys.exc_type, ':', sys.exc_value

The solution is to set sys.exc_type at the moment the exception
handler is entered. (Note that internally in the interpreter, there
is no confusion between exceptions occurring in nested pieces of code
-- it keeps them on its internal stack.) Because of the way the
interpreted code is generated, the interpreter actually assigns to
sys.exc_type each time it finds there are except clauses to consider,
just before evaluating the first clause's exception or list of
exceptions. This is not done for finally clauses -- these are treated
completely different from except clauses. I know I have changed the
policy that determines when sys.exc_type is set several times; in my
own, still experimental version (which I call 0.9.6) it is set
independent of whether the exception is being handled or not. The
simplest case that sets it is:

try:
1/0
except:
pass

It also works with "except RuntimeError:" or "except EOFError:" instead
of "except:". I mayu have changed this since 0.9.4 (the latest
distributed version) in order to support the new tracer/debugger
feature.

It is debatable whether sys.exc_type should be set when there is no
except clause and the user gets thrown back in the interactive main
loop. This isn't done currently, because I haven't encountered a need
for it yet, but I could easily added if there is justified demand. It
might be better to use different variable names in this case: I
already save the traceback info, under the name sys.last_traceback.
The distinction is useful: like sys.exc_type, sys.exc_traceback
contains rather volatile information (overwritten each time an
exception gets handled), while sys.last_traceback remains valid until
the next primary prompt, at least.

In fact, this made so much sense that I've gone to the source and
added sys.last_type and sys.last_value. I've also documented all six
variables in the Library Reference. So there's no need to change
existing, working code that uses any of these, unless this message
triggers a discussion that results in a cleaner design or better names
or whatever other changes might be needed -- folks, speak now or be
silent forever! :-)

--Guido van Rossum, CWI, Amsterdam <guido@cwi.nl>
"One's never alone with a rubber duck."