Search code examples
pythonexceptiontraceback

How to print an exception when I'm not handling it?


I have the following code:

# exc is a local variable of type Exception
# This is not inside an except block
if isinstance(exc, ClientError):
    logging.debug("ClientError raised while loading %s:\n%s", package.id, traceback.format_exc())
    continue

When this code is run and exc is of type ClientError, format_exc() just prints out NoneType: None because no exception is currently being handled (the code is not inside an except block). Luckily there appears to be the format_exception method on traceback that isn't coupled to whatever the current exception being handled is, but in order to call it I need to extract the type, values, and tb from my exception variable. How do I do this?


Solution

  • How is exc being produced? If it is being returned from some function without the corresponding stack then it is not possible to produce the correct frames anyway. On top of that, it is not possible to generate a Traceback object without going deep into ctypes, so this is likely not what is desired.

    If what you are after is actually the stack at where the exception was logged, making use of inspect.currentframe and traceback.format_stack may produce what you might be after. However, as mentioned, you will need to get the frames as close to where the error occurred. Consider this example:

    import traceback
    import inspect
    import logging
    
    
    class Client:
        pass
    
    
    class ClientError(Exception):
        pass
    
    
    def get_client(name):
        if name is None:
            return ClientError('client must have a name')
        return Client()
    
    
    def connect(target, name=None):
        exc = get_client(name)
        if isinstance(exc, ClientError):
            frames = inspect.currentframe()
            logging.debug("ClientError raised while loading %s:\n%s",
                target, ''.join(traceback.format_stack(frames)))
    
    
    def main():
        connect('somewhere')
    
    
    if __name__ == '__main__':
        logging.basicConfig(level=logging.DEBUG)
        main()
    

    Executing this will produce the following output:

    DEBUG:root:ClientError raised while loading somewhere:
      File "foo.py", line 34, in <module>
        main()
      File "foo.py", line 30, in main
        connect('somewhere')
      File "foo.py", line 26, in connect
        target, ''.join(traceback.format_stack(frames)))
    

    Note that the stack ends exactly where the call is done, as the return value of the current_frame is bounded to frames. This is why the stack should be generated and formatted at where it was produced, and step back one level. Consider these updated functions:

    def get_client(name):
        if name is None:
            return (
                ClientError('client must have a name'),
                traceback.format_stack(inspect.currentframe().f_back),
            )
        return Client(), None
    
    
    def connect(target, name=None):
        exc, frames = get_client(name)
        if isinstance(exc, ClientError):
            stack = ''.join(frames)
            logging.debug("ClientError raised while loading %s:\n%s",
                target, stack)
    

    Execution

    $ python foo.py 
    DEBUG:root:ClientError raised while loading somewhere:
      File "foo.py", line 37, in <module>
        main()
      File "foo.py", line 33, in main
        connect('somewhere')
      File "foo.py", line 25, in connect
        exc, frames = get_client(name)
    

    Note how the trace ends at the function that produced the exception.