Dear lazy web...
I've had this code sitting around as a wtf.py
for a while. I've been meaning to understand what's going on and write a blog post about it for a while, but I'm lacking the time. Now that I have a few minutes, I actually sat down to look at it and I think I figured it out:
from contextlib import contextmanager
@contextmanager
def bad():
print(&aposin the context manager&apos)
try:
print("yielding value")
yield &aposvalue&apos
finally:
return print(&aposcleaning up&apos)
@contextmanager
def good():
print(&aposin the context manager&apos)
try:
print("yielding value")
yield &aposvalue&apos
finally:
print(&aposcleaning up&apos)
with bad() as v:
print(&aposgot v = %s&apos % v)
raise Exception(&aposexception not raised!&apos) # SILENCED!
print("this code is reached")
with good() as v:
print(&aposgot v = %s&apos % v)
raise Exception(&aposexpection normally raised&apos)
print("NOT REACHED (expected)")
For those, like me, who need a walkthrough, here's what the above does:
-
define a
bad
context manager (the things you use with with statements) with contextlib.contextmanager) which:- prints a debug statement
- return a value
- then returns and prints a debug statement
-
define a
good
context manager in much the same way, except it doesn't return, it just prints statement -
use the
bad
context manager to show how it bypasses an exception -
use the good context manager to show how it correctly raises the exception
The output of this code (in Debian 11 bullseye, Python 3.9.2) is:
in the context manager
yielding value
got v = value
cleaning up
this code is reached
in the context manager
yielding value
got v = value
cleaning up
Traceback (most recent call last):
File "/home/anarcat/wikis/anarc.at/wtf.py", line 31, in <module>
raise Exception('expection normally raised')
Exception: expection normally raised
What is surprising to me, with this code, is not only does the exception not get raised, but also the return
statement doesn't seem to actually execute, or at least not in the parent scope: if it would, this code is reached
wouldn't be printed and the rest of the code wouldn't run either.
So what's going on here? Now I know that I should be careful with return
in my context manager, but why? And why is it silencing the exception?
The reason why it's being silenced is this little chunk in the with
documentation:
If the suite was exited due to an exception, and the return value from the exit() method was false, the exception is reraised. If the return value was true, the exception is suppressed, and execution continues with the statement following the with statement.
This feels a little too magic. If you write a context manager with __exit__()
, you're kind of forced to lookup again what that API is. But the contextmanager
decorator hides that away and it's easy to make that mistake...
Credits to the Python tips book for teaching me about that trick in the first place.
from Planet Python
via read more
No comments:
Post a Comment