I know that in general python only makes new scopes for classes, functions etc., but I'm confused by the as statement in a try/except block or context manager. Variables assigned inside the block are accessible outside it, which makes sense, but the variable bound with as itself is not.
So this fails:
try:
raise RuntimeError()
except RuntimeError as error:
pass
print(repr(error))
but this succeeds:
try:
raise RuntimeError()
except RuntimeError as e:
error = e
print(repr(error))
What's going on with the variable bound with as, and why don't normal python scoping rules apply? The PEP indicates that it's just a normally bound python variable, but that doesn't seem to be the case.
This is a documented exception to the normal rules that *specifically applies to try-except statements, from the language reference:
When an exception has been assigned using as target, it is cleared at the end of the except clause. This is as if:
except E as N: foowas translated to
except E as N: try: foo finally: del NThis means the exception must be assigned to a different name to be able to refer to it after the except clause. Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs.
As noted, the reason this occurs to prevent reference cycles.
Note, this only applies to a try - except compound statement, as is not an independent statement, it is a part of different, independent compound statements (with and try-except)
As explained in PEP 3110, as well as current documentation, variables bound with as in an except block are explicitly and specially cleared at the end of the block, even though they share the same local scope. This improves the immediacy of garbage collection. The as syntax was originally not available for exceptions in 2.x; it was backported for 2.6, but the old semantics were preserved.
The same does not apply to with blocks:
>>> from contextlib import contextmanager
>>> @contextmanager
... def test():
... yield
...
>>> with test() as a:
... pass
...
>>> a # contains None; does not raise NameError
>>>
>>> def func(): # similarly within a function
... with test() as a:
... pass
... return a
...
>>> func()
>>>
The behaviour is specific to the except block, not to the as keyword.