
Suppose exceptions have an optional "context" attribute, which is set when the exception is raised in the context of handling another exception. Thus: def a(): try: raise AError except: raise BError yields an exception which is an instance of BError. This instance would have as its "context" attribute an instance of AError. Or, in a more complex case: def compute(): try: 1/0 except Exception, exc: log(exc) def log(exc): try: file = open('error.log') # oops, forgot 'w' print >>file, exc file.close() except: display(exc) def display(exc): print 'Aaaack!', ex # oops, misspelled 'exc' Today, this just gives you a NameError about 'ex'. With the suggested change, you would still get a NameError about 'ex'; its 'context' attribute would show that it occurred while handling an IOError on error.log; and this IOError would have a 'context' attribute containing the original ZeroDivisionError that started it all. What do you think? -- ?!ng

[Ka-Ping Yee]
Suppose exceptions have an optional "context" attribute, which is set when the exception is raised in the context of handling another exception. Thus:
def a(): try: raise AError except: raise BError
yields an exception which is an instance of BError. This instance would have as its "context" attribute an instance of AError. [...]
I like the idea, but I'm not sure about the consequences, and I'm not sure how it can be defined rigorously. Would it only happen when something *in* an except clause raises an exception? Which piece of code would be responsible for doing this? Try to come up with a precise specification and we'll talk. -- --Guido van Rossum (home page: http://www.python.org/~guido/)

Guido van Rossum wrote:
[Ka-Ping Yee]
Suppose exceptions have an optional "context" attribute, which is set when the exception is raised in the context of handling another exception. Thus:
def a(): try: raise AError except: raise BError
yields an exception which is an instance of BError. This instance would have as its "context" attribute an instance of AError.
[...]
I like the idea, but I'm not sure about the consequences, and I'm not sure how it can be defined rigorously. Would it only happen when something *in* an except clause raises an exception? Which piece of code would be responsible for doing this?
Try to come up with a precise specification and we'll talk.
If a new exception is raised (e.g., not a bare 'raise') while a current exception is active (e.g., sys.exc_info() would return something other than a tuple of None), then the new exception is made the active exception and the now old exception is assigned to the new exception's context attribute to be the old exception. -Brett

On Thu, 12 May 2005, Brett C. wrote:
Guido van Rossum wrote:
Try to come up with a precise specification and we'll talk.
If a new exception is raised (e.g., not a bare 'raise') while a current exception is active (e.g., sys.exc_info() would return something other than a tuple of None), then the new exception is made the active exception and the now old exception is assigned to the new exception's context attribute to be the old exception.
Yeah, i think that's basically all there is to it. I'll go have a peek at the interpreter to see if i'm forgetting something. -- ?!ng

At 07:36 PM 5/12/2005 -0500, Ka-Ping Yee wrote:
On Thu, 12 May 2005, Brett C. wrote:
Guido van Rossum wrote:
Try to come up with a precise specification and we'll talk.
If a new exception is raised (e.g., not a bare 'raise') while a current exception is active (e.g., sys.exc_info() would return something other than a tuple of None), then the new exception is made the active exception and the now old exception is assigned to the new exception's context attribute to be the old exception.
Yeah, i think that's basically all there is to it. I'll go have a peek at the interpreter to see if i'm forgetting something.
I think the main problem is going to be that (IIUC), Python doesn't "know" when you've exited an 'except:' clause and are therefore no longer handling the exception. sys.exc_info() still gives you the exception you just caught. I think that a lot of the questions Guido brought up are directly related to this. Also, what about code like this: try: doSomething() except SomeError: pass doSomethingElse() Should exceptions raised by doSomethingElse()' be treated as having the SomeError as their context, if it was raised? If I understand correctly, the interpreter cannot currently distinguish between this, and the case where an error is raised inside the 'except' clause.

[Phillip J. Eby]
I think the main problem is going to be that (IIUC), Python doesn't "know" when you've exited an 'except:' clause and are therefore no longer handling the exception.
But the compiler knows and could insert code to maintain this state.
sys.exc_info() still gives you the exception you just caught. I think that a lot of the questions Guido brought up are directly related to this.
Right.
Also, what about code like this:
try: doSomething() except SomeError: pass
doSomethingElse()
Should exceptions raised by doSomethingElse()' be treated as having the SomeError as their context, if it was raised?
If I understand correctly, the interpreter cannot currently distinguish between this, and the case where an error is raised inside the 'except' clause.
Indeed the interpreter currently doesn't distinguish between these, but I think it ought to for the purposes of this proposal. -- --Guido van Rossum (home page: http://www.python.org/~guido/)

[Brett C.]
If a new exception is raised (e.g., not a bare 'raise') while a current exception is active (e.g., sys.exc_info() would return something other than a tuple of None), then the new exception is made the active exception and the now old exception is assigned to the new exception's context attribute to be the old exception.
Define "raise". Does that involve a raise statement? What about 1/0? What if you call a method that executes 1/0? What if that method catches that exception? What about the StopIteration conceptually raised by next() called by the for-loop implementation? (Often it doesn't get instantiated at all when the next() method belongs to a built-in iterator.) I believe there are (at least) two use cases: (1) I catch some low-level exception (e.g. socket.error) and turn it into a high-level exception (e.g. an HTTPRequestFailed exception). (2) I write some exception handling code and somehow a bug in the handler (or an uncooperative environment, e.g. a full disk) causes the exception handling code to trip over an exception. I'm fairly certain (but not 100%) that Ping meant to include both use cases. -- --Guido van Rossum (home page: http://www.python.org/~guido/)

On Thu, 12 May 2005, Guido van Rossum wrote:
Define "raise". Does that involve a raise statement?
Not necessarily; it could be a raise statement or an inadvertently triggered exception, such as in the example code i posted.
What about 1/0?
That counts.
What if you call a method that executes 1/0?
That counts too.
What if that method catches that exception?
Did you mean something like this? def handle(): try: open('spamspamspam') except: catchit() # point A ... def catchit(): try: 1/0 except: pass Then there's no exception to propagate, so it doesn't matter. Once we're get to point A, the division by zero is long forgotten.
What about the StopIteration conceptually raised by next() called by the for-loop implementation?
It's "caught" by the for-loop, so to speak, so it never gets out. Conceptually, the for-loop expands to: while 1: try: item = it.next() except StopIteration: break # body of loop goes here The 'break' can't possibly cause an exception, so the StopIteration exception isn't retained.
I believe there are (at least) two use cases:
(1) I catch some low-level exception (e.g. socket.error) and turn it into a high-level exception (e.g. an HTTPRequestFailed exception).
(2) I write some exception handling code and somehow a bug in the handler (or an uncooperative environment, e.g. a full disk) causes the exception handling code to trip over an exception.
I'm fairly certain (but not 100%) that Ping meant to include both use cases.
Yes, though i did not expect to provide any mechanism for distinguishing the two cases. Do you think such a mechanism would be necessary? -- ?!ng

[Guido]
What if that method catches that exception?
[Ka-Ping Yee]
Did you mean something like this?
def handle(): try: open('spamspamspam') except: catchit() # point A ...
def catchit(): try: 1/0 except: pass
Then there's no exception to propagate, so it doesn't matter. Once we're get to point A, the division by zero is long forgotten.
But at what point does the attaching happen? If I catch the ZeroDivisionException inside catchit() and inspects its context attribute, does it reference the IOError instance raised by open('spamspamspam')? This could potentially cause a lot of extra work: when an inner loop that raises and catches lots of exceptions is invoked in the context of having caught an exception at some outer level, the inner loop keeps attaching the outer exception to each exception raised.
Yes, though i did not expect to provide any mechanism for distinguishing the two cases. Do you think such a mechanism would be necessary?
No, I was just trying to figure out what you meant when you said "raise". It's clear now. -- --Guido van Rossum (home page: http://www.python.org/~guido/)

Guido van Rossum wrote:
[Guido]
What if that method catches that exception?
[Ka-Ping Yee]
Did you mean something like this?
def handle(): try: open('spamspamspam') except: catchit() # point A ...
def catchit(): try: 1/0 except: pass
Then there's no exception to propagate, so it doesn't matter. Once we're get to point A, the division by zero is long forgotten.
But at what point does the attaching happen? If I catch the ZeroDivisionException inside catchit() and inspects its context attribute, does it reference the IOError instance raised by open('spamspamspam')?
Yes, at least in the way I am imagining this being implemented. I was thinking that when an exception happens, the global exception variable is checked to see if it has a value. If it does that gets assigned to the new exception's 'context' attribute and the new exception gets assigned to the global exception variable.
This could potentially cause a lot of extra work: when an inner loop that raises and catches lots of exceptions is invoked in the context of having caught an exception at some outer level, the inner loop keeps attaching the outer exception to each exception raised.
[this also contains a partial answer to Philip's email also in this thread] Maybe, but as long as caught exceptions get cleared that should be an issue. Would this be solved if, when an 'except' branch is exited, exceptions are cleared? So, in the above example, once the 'pass' is hit in catchit() no exception is considered active any longer. This could be done with a CLEAR_EXC opcode very easily inserted at the end of an 'except' branch by the compiler. This would require explicit re-raising of exceptions to keep them alive after an 'except' ends, but I think that is actually a good idea and since this might all wait until Python 3000 anyway we don't need to worry about the semantic change. -Brett

[Brett C.]
Maybe, but as long as caught exceptions get cleared that should be an issue. Would this be solved if, when an 'except' branch is exited, exceptions are cleared? So, in the above example, once the 'pass' is hit in catchit() no exception is considered active any longer. This could be done with a CLEAR_EXC opcode very easily inserted at the end of an 'except' branch by the compiler.
Sure, but that would be backwards incompatible. There's plenty of code that expects sys.exc_info() to continue to return the caught exception *outside* the except block. This is all incredibly tricky, to some extent for backwards compatibility reasons (please read the source code for maintaining the exc_info data!). In Python 3000, I think we can get rid of sys.exc_info() altogether once we place the traceback in the exception object as the 'traceback' attribute: if you want this info, all you need is write except SomeException, err: # now type is err.__class__, value is err, and traceback is err.traceback. If you want to have this with an "except:" clause, you can just catch 'Exception' or perhaps 'BaseException'. This isn't possible in Python 2.x since there's no single base class. -- --Guido van Rossum (home page: http://www.python.org/~guido/)

Guido van Rossum wrote:
[Brett C.]
Maybe, but as long as caught exceptions get cleared that should be an issue. Would this be solved if, when an 'except' branch is exited, exceptions are cleared? So, in the above example, once the 'pass' is hit in catchit() no exception is considered active any longer. This could be done with a CLEAR_EXC opcode very easily inserted at the end of an 'except' branch by the compiler.
Sure, but that would be backwards incompatible.
Right. None of what I am discussing here I would expect to be implemented any sooner than Python 3000.
There's plenty of code that expects sys.exc_info() to continue to return the caught exception *outside* the except block. This is all incredibly tricky, to some extent for backwards compatibility reasons (please read the source code for maintaining the exc_info data!).
In Python 3000, I think we can get rid of sys.exc_info() altogether once we place the traceback in the exception object as the 'traceback' attribute: if you want this info, all you need is write
except SomeException, err: # now type is err.__class__, value is err, and traceback is err.traceback.
Right, that is kind of the end goal in my mind.
If you want to have this with an "except:" clause, you can just catch 'Exception' or perhaps 'BaseException'. This isn't possible in Python 2.x since there's no single base class.
Right. Once again I am only thinking about Python 3000. -Brett

On May 12, 2005, at 6:32 PM, Ka-Ping Yee wrote:
Suppose exceptions have an optional "context" attribute, which is set when the exception is raised in the context of handling another exception. Thus:
def a(): try: raise AError except: raise BError
yields an exception which is an instance of BError. This instance would have as its "context" attribute an instance of AError.
I think it's a bad idea to have this happen automatically. Many times if an exception is raised in the except clause, that doesn't necessarily imply it's related to the original exception. It just means there's a bug in the exception handler. Take the divide by 0 example: try: doABunchOfStuff() except: 1/0 If you're going to do anything useful with the chained exception information (such as have it printed by the default exception printer), it'd be best to not print a bunch of irrelevant backtraces for all exceptions up the stack. The reason that doABunchOfStuff failed is not important. What is important is only that you had a divide by 0. In my mind it's much better to be explicit about your intentions, via something like: try: raise AError except Raiseable, e: raise BError(cause=e) Of course you can already do similar with current python, it just can't be spelled as nicely, and the default traceback printer won't use the info: try: raise AError except: newException = BError() newException.cause=sys.exc_info() raise newException James

[James Y Knight ]
I think it's a bad idea to have this happen automatically. Many times if an exception is raised in the except clause, that doesn't necessarily imply it's related to the original exception. It just means there's a bug in the exception handler.
Yeah, but especially in that case I think it would be nice if the traceback printed by the system (if all this doesn't get caught at an outer level) could show both the traceback from the handler and the traceback that it was trying to handle -- I've had many occasions where a trivial bug in the handler blew away the original traceback which was shy enough to make repeating it a pain. -- --Guido van Rossum (home page: http://www.python.org/~guido/)
participants (5)
-
Brett C.
-
Guido van Rossum
-
James Y Knight
-
Ka-Ping Yee
-
Phillip J. Eby