Traceback showing local variable values at call site (hacking frame.f_locals, frame.f_lineno etc)

Hi,

Given:

def affine(x, y, z):
    return x + mul(y, z)

def mul(p, q):
    return p * q

def test():
    a = 5
    b = 6
    c = 'fake' 
    d = affine(a, b, c)

test()

Running it I get the following traceback. The trailing comments are additions which I would like to implement:

henry@henry-gs65:~$ python tb.py 
Traceback (most recent call last):
  File "tb.py", line 13, in <module>
    test()
  File "tb.py", line 11, in test
    d = affine(a, b, c)  # a = 5, b = 6, c = 'fake'
  File "tb.py", line 2, in affine
    return x + mul(y, z) # y = 6, z = 'fake'
TypeError: unsupported operand type(s) for +: 'int' and 'str'

I would like to write a custom sys.excepthook which provides comments showing the values of the local variables specifically at the call sites of lower frames. The difficulty is that, while each frame object provides frame.f_locals and frame.f_globals and also frame.f_lineno identifying the current execution line, there seems no direct way to get the identifiers of the currently executing line.

My thoughts are: grab the code text using inspect.getsourcelines(frame.f_code) and then get the executing line(s) using frame.f_lineno. Then parse the code string using ast.parse, and then retrieve the identifiers from the parse tree.

This seems like it would be quite error proned though. Is there a better way?

Yes, there is a better way than simply using frame.f_lineno.

friendly uses Alex Hall’s executing to localize the exact part of the code in each frame that causes the error, and then finds the name of each object involved. This is somewhat akin to the latest improvements of Python (3.11) that show the part of the line contributing to the traceback, but even better as it works when a statement spans multiple lines - so, by using executing you are not limited to the single line pointed at by frame.f_lineno. executing is used by stack_data (also from Alex Hall), to provide nicely formatted tracebacks; stack_data is used by iPython for its tracebacks.

Here’s a screen capture of an example, similar to yours, except that I intentionally wrote statements spanning multiple lines.


Asking “where()” shows the information from the first and last frame.


As you can see, we do have the value of all the objects involved.

It is possible to get the similar information from all the frames:

2 Likes

Just to clarify: the changes made in 3.11 also include multiple lines of span, is just that the interpreter just shows the first by default:

In [1]: def affine(x, y, z):
   ...:        return x + mul(y,
   ...:                         z)
   ...:

In [4]: try:
   ...:     affine(1, 2, 3)
   ...: except Exception as e:
   ...:     f = e
   ...:

list(affine.__code__.co_positions())
Out[7]:
[(1, 1, 0, 0),
 (2, 2, 14, 15),
 (2, 2, 18, 21),
 (2, 2, 18, 21),
 (2, 2, 18, 21),
 (2, 2, 18, 21),
 (2, 2, 18, 21),
 (2, 2, 18, 21),
 (2, 2, 22, 23),
 (3, 3, 24, 25),
 (2, 3, 18, 26),
 (2, 3, 18, 26),
 (2, 3, 18, 26),
 (2, 3, 18, 26),
 (2, 3, 18, 26),
 (2, 3, 18, 26),
 (2, 3, 18, 26),
 (2, 3, 14, 26),
 (2, 3, 14, 26),
 (2, 3, 7, 26)]

 line_start, line_end, col_start, col_end = list(affine.__code__.co_positions())[f.__traceback__.tb_lasti // 2

In [34]: line_start, line_end, col_start, col_end
Out[34]: (2, 3, 18, 26)

In [36]: code.split("\n")[(line_start-1)][col_start:]
Out[36]: 'mul(y,'

In [42]: code.split("\n")[(line_end-1)][:col_end]
Out[42]: '                        z)'
1 Like

Wow! That looks awesome - exactly what I was looking for. Thanks so much Mr. Roberge. Just installed it, will have a look.

Hi Mr. Roberge,

I am wondering if there may be any issue specifically when user code calls a third-party function which itself filters out traceback frames. In this example, tf.matmul doesn’t actually have any frame corresponding to it in the traceback.

import tensorflow as tf
import friendly_traceback as ft

ft.install()

def tf_test():
    a = tf.random.uniform([3,5,7], 0, 100, dtype=tf.float32)
    b = tf.random.uniform([3,5,7], 0, 100, dtype=tf.float32)
    return tf.matmul(a, b)

tf_test()

This might be a tricky one because tensorflow has a lot of dispatching code, and it actually has a traceback decorator which filters out lots of frames. So, the next frame below the ‘tf_test’ frame is not the one produced by ‘tf.matmul’, but a few levels below that.

Running this, I see:

(tf) henry@henry-gs65:pyctb$ python tf_test.py 
Traceback (most recent call last):
  File "HOME:/ai/projects/pyctb/tf_test.py", line 11, in <module>
    tf_test()
  File "HOME:/ai/projects/pyctb/tf_test.py", line 9, in tf_test
    return tf.matmul(a, b)
  File "LOCAL:/tensorflow/python/util/traceback_utils.py", line 153, in error_handler
    raise e.with_traceback(filtered_tb) from None
  File "LOCAL:/tensorflow/python/framework/ops.py", line 7209, in raise_from_not_ok_status
    raise core._status_to_exception(e) from None  # pylint: disable=protected-access
InvalidArgumentError: {{function_node __wrapped__BatchMatMulV2_device_/job:localhost/replica:0/task:0/device:GPU:0}} Matrix size-incompatible: In[0]: [3,5,7], In[1]: [3,5,7] [Op:BatchMatMulV2]

    An exception of type `InvalidArgumentError` is a subclass of `Exception`.
    Nothing more specific is known about `InvalidArgumentError`.
    The inheritance is as follows:
    
        InvalidArgumentError -> OpError -> Exception
    
    
    All built-in exceptions defined by Python are derived from `Exception`.
    All user-defined exceptions should also be derived from this class.
    
    Execution stopped on line `11` of file 'HOME:/ai/projects/pyctb/tf_test.py'.
    
        7|     a = tf.random.uniform([3,5,7], 0, 100, dtype=tf.float32)
        8|     b = tf.random.uniform([3,5,7], 0, 100, dtype=tf.float32)
        9|     return tf.matmul(a, b)
       10| 
    -->11| tf_test()
           ^^^^^^^^^

            tf_test:  <function tf_test>
        
    Exception raised on line `7209` of file 'LOCAL:/tensorflow/python/framework/ops.py'.
    
       7207| def raise_from_not_ok_status(e, name):
       7208|   e.message += (" name: " + name if name is not None else "")
    -->7209|   raise core._status_to_exception(e) from None  # pylint: disable=protected-access

            e:  _NotOkStatusException()
            global core:  <module tensorflow.python.eager.core>
                from LOCAL:/tensorflow/python/eager/core.py
            core._status_to_exception:  <function _status_to_exception>
        
(tf) henry@henry-gs65:pyctb$ 

Notice that there is no annotation for tf.matmul(a, b) line.

Apologies for the non-trivial example - installing tensorflow is a big pain. But, more generally, I just wondered if friendly_traceback depends on the lower frame for producing information about the current frame?

Thanks again

By default, friendly/friendly-traceback only shows detailed information from the first and last frame. In the interactive mode, you can include all the frames, as I have shown. I could fairly easily add an option to the install() method to show all the frames. If you would like to have this, file an issue. If you do so, I should be able to get this implemented this weekend.

Yes, and for Python 3.11+, executing uses this improved location information instead of relying on its method that works extremely well for older Python versions.

As you allude below, this is example is not (currently) a good example of what friendly-traceback can do. I am disappointed that you will avoid it, as I find the information you provide to be generally very helpful.

Yes, the first sentence is a mistake that needs to be fixed; the second sentence was actually added after I read one of your comments on this site about user-defined exceptions needing to be derived from Exception rather than BaseException. These two sentences (and the inheritance hierarchy mentioned below) are only shown when one deals with an exception not already known by Friendly-traceback, something I have not had to dealt with in quite a while as I add more examples.

Eventually, I want to include an explanation of what all (common) exception means, starting with those from the standard library, and eventually including those from commonly used libraries (including pandas, numpy, and eventually others like Tensorflow). In the mean time, when an exception is not known, I thought it would be useful to show the inheritance, rather than simply having something like “I don’t know what this exception means”, which used to be the case.

Yes, I do agree that the current output is not a good showcase of what friendly-traceback can do.

One can choose different configuration options, to only show part of all the information available from friendly-traceback. For beginner code, the output is not usually as verbose as this, and, I believe, it is more useful than this example implies. And, friendly-traceback is primarily designed to help beginners.

Final comment: my original reply was not to suggest that the OP should use friendly-traceback. Rather, I was suggesting that the OP might want to use executing instead of frame.f_ineno to find the location, and I was simply using friendly-traceback to show what it could look like.

Hi Mr. Roberge,

Just a follow up question as I’m getting more familiar with this problem. Originally I hadn’t really thought what to do in the following situation:

def add(a, b):
    return a + b

def fadd(a, b):
    return 'fake'

def test3():
    return add(add(2, 4), fadd(3, 5))

test3()

I was experimenting with stack_data on this case and found that the FrameInfo for the test3 frame lacks any variables - it doesn’t seem to retain the temporaries created by the evaluation of add(2, 4) and fadd(3, 5). So, it would be difficult to produce a message like:

return add(add(2, 4), fadd(3 ,5))
           ^^^^^^^^   ^^^^^^^^^^
Values:
=> add(2, 4) = 6
=> fadd(3, 5) = 'fake'

let’s say. At least, it doesn’t seem straightforward to do this just from the information in one FrameInfo object at a time. Any thoughts on this case?

Thanks again,

Henry