e = 1
try:
...
except Exception as e:
...
print(e)
It will print 1 if no exception is raised, but if an exception is raised, it will raise NameError (UnboundLocalError inside a function) on the print(e) line.
The exception handler will clobber the previous value of e with the exception instance, and then at the end of the exception block, delete the exception instance (to avoid reference cycles that would frequently result from keeping it alive), leaving e unassigned.
Now that PEP 709 (comprehension inlining) has landed, we have both a language precedent and the compiler machinery to support âsub-scopesâ where a temporary variable can be isolated from a variable of the same name in the rest of the function. This means it would now be pretty easy to change the above behavior such that print(e) would always print 1, regardless of whether an exception were raised.
Is this an improvement? Does it matter to anyone either way? Does this require a PEP?
This seems like a clear improvement to me; there have been questions about the behaviour before, and itâs hard to imagine that anyone would explicitly want locals that existed outside the try to be removed. Although itâs a pretty rare case, implementing an actual separate scope (which is functionally what save/restore accomplishes, yes?) sounds much more elegant than the current del (which could also be argued to be needlessly implicit).
Yes, I always thought the implicit del was a bit of a wart on the language. Making it behave like comprehension variables would be cleaner.
Weâll have to think about backwards compatibility. I can think of two possible issues:
Some code that would previously have thrown a NameError now will not.
Some resources may be kept around for longer because they were previously implicitly deleted. If this happens in a module or class scope, the variable may even be kept around for the lifetime of the program. Contrived example:
e = [1] * 100_000 # really big list
try: 1/0
except ZeroDivisionError as e: pass # implicitly clean up the huge list
I think both of those cases are obscure enough that we can afford to change them, but weâll have to think carefully about it.
Yes, I think a change in the core behavior of the language requires a PEP.
e = 1
try:
...
except Exception as e:
...
print(e)
Isnât that e isnât 1, as that follows normal assignment / scoping rules in Python as once a variable is assigned in the same scope to something else but then the new assignment is deleted it doesnât magically return to itâs previous assignment. Whatâs confusing is that e isnât an instance of Exception as the delete at the end of an exception block is special, it doesnât happen during other assignments which involve a block, e.g. for and with.
Now I know the reasoning for why this happens, pretty much every developer at some points hits this UnboundLocalError and eventually finds the relevant docs.
If there is a âspecialâ solution to this I would strongly argue it is that this exception is somehow made special to tell you about the implicit delete or give you a link to the documentation and not some new weird scoping rule special to Exceptions.
Scoping is already hard to accurately mentally model[1] and it seems itâs going to get harder with PEP 695[2] and PEP 649 in Python 3.12 and 3.13 respectively, but at least they are solving difficult problems and making Python systemically better rather than solving a mild nuisance the first time you some across it. My 2 cents anyway.
IIRC the del e was added when we added exception chaining (though I canât find a trace of this in PEP 3134, so I may misremember the exact timing), to address the problem of reference cycles through the locals (locals â err â traceback â frame â locals). Before then e would not only overwrite the prior definition, it would survive until after the except clause â unless, of course, the except clause wasnât executed.
Like Damian, I believe we should think long and hard before we introduce more one-off scoping exceptions â as we learned during the PEP 695 discussions, Python scopes are already quite weird (despite the âLEGBâ meme, which didnât quite capture all the rules even before PEP 695). Maybe we should consider a more general way of introducing local scopes, and then specify the except scope in terms of those. Ideally PEP 695 scopes would also be defined in those terms, as well as comprehension scopes â though the latter are even weirder (see PEP 572), so who knows if thatâs even possible.
And surely introducing a local scope concept disconnected from function scopes will also increase the pressure to âfixâ the scope of for variables, and possibly with variables, and whatâs next? Walrus variables?
" When an exception has been assigned using as target, it is cleared at the end of the except clause." appears to have been added in 3.0. (There is no âaddedâ note and not in 2.7 that I could see.)
Iâm not a fan of these exceptions to the usual behaviour of Python (also PEP 695 ). The reason is mostly that it creates a dilemma when teaching these structures: Do you give an incomplete account of the rules, or do you risk confusing students by mentioning the exceptions to the rules? I find the latter can create a sense of anxiety (âThereâs this thing I have to rememberâŚâ) through mere existence of such exceptions, even if they are perfectly logical and easily understood.
So instead of making except ... as ... even more divergent from with ... as ... and normal scoping rules, perhaps itâs possible to fix the issue going the other way, working around the reference cycles problem by clearing e.g. the traceback in the exception at the end of the block, or the locals in the traceback?
I would DEFINITELY dislike this. The object is the same, but now it has less information in it? No thanks. With the current system, thereâs a clear workaround: except Exception as ee: e = ee and then it gets retained. How would you force it to be retained if the object itself is being mutated?
That was just an off-the-cuff example. My point is: this thread is talking about fixing a symptom of a deeper problem (i.e. the reference cycles, or whatever else it may be if it wasnât that). Why not put the effort in fixing the original cause in a better way?
Iâm curious what sort of thing youâre envisioning here. Do you mean more general syntax that introduces a local scope (e.g. as straw man, something like a scope: block that isolates local variables within it), and then the documentation can define the behavior of except blocks and comprehensions with reference to scope: blocks?
This does surface the question (not explicitly specified in my OP) whether all variables assigned within an except handler would be scoped to just the handler block, or just the exception-capturing variable. I had intended the latter (and I donât think the former is feasible or a good idea, it would have much wider backwards-compatibility problems). But this does make it a pretty weird case, that I donât think would be specifiable in terms of a more general ânested scopesâ feature.
I think really the only argument in favor is that the deletion is already a weird case, and this would be still weird but arguably a nicer version of it.
Thatâs a reasonable position. I agree that it would be strange for except Exception as e: to introduce a locally-scoped temporary variable, when no other block does that.
That would indeed be ideal, if there were a reasonable way to do that. Iâm not sure there is.
Maybe when evaluating e it could still raise an exception (so where the error is coming from doesnât get obscured) with an Exception Note that says why and where e was deleted?
It was always possible to push the value of e to the stack (or NULL if it isnât defined) and restore it on exit from the handler, instead of wiping it out. It doesnât require a new scope. I think it wasnât done to avoid confusion about what the value of e is after an except block was executed. I doubt the reason was performance, since weâre talking about an exception handling code path.
Yes, it was always possible, although it would not previously have been easy, and now it is. Doing this correctly in all scopes requires a number of compiler changes that were made as part of PEP 709. (A variant of LOAD_FAST that is OK with loading NULL, a variant of STORE_FAST that isnât assumed to store a non-NULL, and the capacity to temporarily use fast-locals in class and module scope where they arenât normally used.)
I agree that the suggestion in this thread should not really be described as a ânew scopeâ at all, since it would only apply to a single variable, not all assignments made within a block.
Hmm. Intuitively I would have thought traceback â frame is the point to break, since the frame is what naturally loses a reference at the end of scope when returning from the function, and because frame and locals exist even when there isnât an exception. But Iâve barely looked at the C code, so.
Tracebacks need to keep the frame alive, since they are most often used after the frame has already exited. If the frame was weakly referenced, it would not be available to traceback printers, post-mortem debuggers, and the like.
Possibly off-topic note from end-user perspective, I wish as x was consistent:
>>> with open("/etc/passwd") as f: pass
...
>>> f
<_io.TextIOWrapper name='/etc/passwd' mode='r' encoding='UTF-8'>
and yet
>>> try: 1/0
... except Exception as e: pass
...
>>> e
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'e' is not defined
Likewise:
>>> with open("/etc/passwd") as os.environ: pass
...
>>> os.environ
<_io.TextIOWrapper name='/etc/passwd' mode='r' encoding='UTF-8'>
and yet
>>> try: 1/0
... except Exception as os.environ: pass
File "<stdin>", line 2
except Exception as os.environ: pass
^
SyntaxError: invalid syntax