Reputation: 31643
I encountered a strange behaviour in Python's with-statement recently. I have a code which uses Python's context managers to rollback configuration changes in __exit__
method. The manager had a return False
value in a finally block in __exit__
. I've isolated the case in following code - the only difference is with the indent of return statement:
class Manager1(object):
def release(self):
pass # Implementation not important
def rollback(self):
# Rollback fails throwing an exception:
raise Exception("A failure")
def __enter__(self):
print "ENTER1"
def __exit__(self, exc_type, exc_val, exc_tb):
print "EXIT1"
try:
self.rollback()
finally:
self.release()
return False # The only difference here!
class Manager2(object):
def release(self):
pass # Implementation not important
def rollback(self):
# Rollback fails throwing an exception:
raise Exception("A failure")
def __enter__(self):
print "ENTER2"
def __exit__(self, exc_type, exc_val, exc_tb):
print "EXIT2"
try:
self.rollback()
finally:
self.release()
return False # The only difference here!
In the code above the rollback fails of with an Exception. My question is, why Manager1
is behaving differently than Manager2
. The exception is not thrown outside of with-statement in Manager1
and why it IS thrown on exit in Manager2
.
with Manager1() as m:
pass # The Exception is NOT thrown on exit here
with Manager2() as m:
pass # The Exception IS thrown on exit here
According to documentation of __exit__
:
If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.
In my opinion in both cases the exit is not returning True, thus the exception should not be supressed in both cases. However in Manager1 it is. Can anyone explain that?
I use Python 2.7.6.
Upvotes: 2
Views: 4132
Reputation: 387587
I think a good way to understand this is by looking at a separate example that is independent of all the context manager stuff:
>>> def test ():
try:
print('Before raise')
raise Exception()
print('After raise')
finally:
print('In finally')
print('Outside of try/finally')
>>> test()
Before raise
In finally
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
test()
File "<pyshell#6>", line 4, in test
raise Exception()
Exception
So you can see that when an exception is thrown within the try
block, any code before the exception is executed and any code inside the finally
block is executed. Apart from that, everything else is skipped. That is because the exception that is being thrown ends the function invocation. But because the exception is thrown within a try
block, the respective finally
block has a final chance to run.
Now, if you comment out the raise
line in the function, you will see that all code is executed, since the function does not end prematurely.
Upvotes: 1
Reputation: 37013
If the finally
clause is activated that means that either the try
block has successfully completed, or it raised an error that has been processed, or that the try
block executed a return
.
In Manager1 the execution of the return
statement as part of the finally
clause makes it terminate normally, returning False
. In your Manager2 class the finally clause still executes, but if it was executed as a result of an exception being raised it does nothing to stop that exception propagating back up the call chain until caught (or until it terminates you program with a traceback).
Manager2.__exit__()
will only return False if no exception is raised.
Upvotes: 5