DEV Community

Alex Becker
Alex Becker

Posted on • Originally published at pydist.com

Python's except quirk

Let me show you my favorite Python quirk. What would you expect this python code to do?

e = 2.718
try:
    1/0
except ZeroDivisionError as e:
    pass
print(e)

If you come to Python from another programming language, you might expect that the except clause introduces a nested scope, so assigning to e in the clause does not effect the pre-existing e variable in the outer scope. However, in python control structures do not generally introduce nested scoped (comprehensions being the exception), so with more python experience you would probably expect this to print a ZeroDivisionError instance.

Actually, in the standard CPython implementation it prints nothing; instead, the last line raises a NameError. Is this a bug? Actually, it was quite intentional. If you look at the bytecode generated by the except clause, you see:

LOAD_CONST 0 (None)
STORE_NAME 1 (e)
DELETE_NAME 1

When control flow exits the except block, python deletes the name from scope. Why? Because the exception holds a reference to the current stack frame, which contains everything in scope. Since python is manages memory primary via reference count, this means that nothing in the current scope will be freed until the next round of garbage collection runs, if at all. The current behavior is a compromise between memory usage, ease of implementation, and cleanliness of the language. It is a bit of a wart, but I think it embodies a one of the things I love about Python: not letting purity get in the way of practicality.

But that only explains the DELETE_NAME instruction. Why does CPython set e to None if it's going to delete it immediately afterwards? Well, imagine you had the same thought as the CPython team, and decided to clean up the exception reference at the end of your except block:

try:
    1/0
except ZeroDivisionError as e:
    ...
    del e

At the end of your except block, CPython will try to delete the name eโ€”which you already deleted! To get around this, CPython assigns e = None before deleting e to guarantee that e exists.

Top comments (0)