breakpoint in except clause doesn't have access to the bound exception

breakpoint() is not a breakpoint in the sense that it halts execution at the exact location of this function call. Instead it's a shorthand for import pdb; pdb.set_trace() which will halt execution at the next line of code (it calls sys.settrace under the covers). Since there is no more code inside the except block, execution will halt after that block has been exited and hence the name err is already deleted. This can be seen more clearly by putting an additional line of code after the except block:

try:
    raise ValueError('test')
except ValueError as err:
    breakpoint()
print()

which gives the following:

$ python test.py 
> test.py(5)<module>()
-> print()

This means the interpreter is about to execute the print() statement in line 5 and it has already executed everything prior to it (including deletion of the name err).

When using another function to wrap the breakpoint() then the interpreter will halt execution at the return event of that function and hence the except block is not yet exited (and err is still available):

$ python test.py 
--Return--
> test.py(5)<lambda>()->None
-> (lambda: breakpoint())()

Exiting of the except block can also be delayed by putting an additional pass statement after the breakpoint():

try:
    raise ValueError('test')
except ValueError as err:
    breakpoint()
    pass

which results in:

$ python test.py 
> test.py(5)<module>()
-> pass
(Pdb) p err
ValueError('test')

Note that the pass has to be put on a separate line, otherwise it will be skipped:

$ python test.py 
--Return--
> test.py(4)<module>()->None
-> breakpoint(); pass
(Pdb) p err
*** NameError: name 'err' is not defined

Note the --Return-- which means the interpreter has already reached the end of the module.


This is an excellent question!

When something strange is going on, I always dis-assemble the Python code and have a look a the byte code.

This can be done with the dis module from the standard library.

Here, there is the problem, that I cannot dis-assemble the code when there is a breakpoint in it :-)

So, I modified the code a bit, and set a marker variable abc = 10 to make visible what happens after the except statement.

Here is my modified code, which I saved as main.py.

try:
    raise ValueError('test')
except ValueError as err:
    abc = 10

When you then dis-assemble the code...

❯ python -m dis main.py 
  1           0 SETUP_FINALLY           12 (to 14)

  2           2 LOAD_NAME                0 (ValueError)
              4 LOAD_CONST               0 ('test')
              6 CALL_FUNCTION            1
              8 RAISE_VARARGS            1
             10 POP_BLOCK
             12 JUMP_FORWARD            38 (to 52)

  3     >>   14 DUP_TOP
             16 LOAD_NAME                0 (ValueError)
             18 COMPARE_OP              10 (exception match)
             20 POP_JUMP_IF_FALSE       50
             22 POP_TOP
             24 STORE_NAME               1 (err)
             26 POP_TOP
             28 SETUP_FINALLY            8 (to 38)

  4          30 LOAD_CONST               1 (10)
             32 STORE_NAME               2 (abc)
             34 POP_BLOCK
             36 BEGIN_FINALLY
        >>   38 LOAD_CONST               2 (None)
             40 STORE_NAME               1 (err)
             42 DELETE_NAME              1 (err)
             44 END_FINALLY
             46 POP_EXCEPT
             48 JUMP_FORWARD             2 (to 52)
        >>   50 END_FINALLY
        >>   52 LOAD_CONST               2 (None)
             54 RETURN_VALUE

You get a feeling what is going on.

You can read more about the dis module both in the excellent documentation or on the Python module of the week site:

https://docs.python.org/3/library/dis.html https://docs.python.org/3/library/dis.html

Certainly, this is not a perfect answer. Actually, I have to sit down and read documentation myself. I am surprised that SETUP_FINALLY was called before the variable abc in the except block was handled. Also, I am not sure what's the effect of POP_TOP - immediately executed after storing the err name.

P.S.: Excellent question! I am super excited how this turns out.