Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I add context to an exception in Python

I would like to add context to an exception like this:

def process(vals):
    for key in vals:
        try:
            do_something(vals[key])
        except Exception as ex:  # base class. Not sure what to expect.
            raise # with context regarding the key that was being processed.

I found a way that is uncharacteristically long winded for Python. Is there a better way than this?

try:
    do_something(vals[key])
except Exception as ex:
    args = list(ex.args)
    if len(args) > 1:
        args[0] = "{}: {}".format(key, args[0])
        ex.args = tuple(args)
    raise # Will re-trhow ValueError with new args[0]
like image 433
elmotec Avatar asked Jul 16 '13 13:07

elmotec


2 Answers

The first item in ex.args is always the message -- if there is any. (Note for some exceptions, such as the one raised by assert False, ex.args is an empty tuple.)

I don't know of a cleaner way to modify the message than reassigning a new tuple to ex.args. (We can't modify the tuple since tuples are immutable).

The code below is similar to yours, except it constructs the tuple without using an intermediate list, it handles the case when ex.args is empty, and to make the code more readable, it hides the boilerplate inside a context manager:

import contextlib

def process(val):
    with context(val):
        do_something(val)

def do_something(val):
    # assert False
    return 1/val

@contextlib.contextmanager
def context(msg):
    try:
        yield
    except Exception as ex:
        msg = '{}: {}'.format(msg, ex.args[0]) if ex.args else str(msg)
        ex.args = (msg,) + ex.args[1:]
        raise

process(0)

yields a stack trace with this as the final message:

ZeroDivisionError: 0: division by zero
like image 167
unutbu Avatar answered Oct 06 '22 07:10

unutbu


You could just raise a new exception:

def process(vals):
    for key in vals:
        try:
            do_something(vals[key])
        except Exception as ex:  
            raise Error(key, context=ex)

On Python 3 you don't need to provide the old exception explicitly, it will be available as __context__ attribute on the new exception object and the default exception handler will report it automatically:

def process(vals):
    for key in vals:
        try:
            do_something(vals[key])
        except Exception:  
            raise Error(key)

In you case, you should probably use the explicit raise Error(key) from ex syntax that sets __cause__ attribute on the new exception, see Exception Chaining and Embedded Tracebacks.


If the only issue is the verbosity of the message-amending code in your question; you could encapsulate it in a function:

try:
    do_something(vals[key])
except Exception:
    reraise_with_context(key=key) # reraise with extra info

where:

import inspect
import sys

def reraise_with_context(**context):
    ex = sys.exc_info()[1]
    if not context: # use locals from the caller scope
       context = inspect.currentframe().f_back.f_locals
    extra_info = ", ".join("%s=%s" % item for item in context.items())
    amend_message(ex, extra_info)
    raise

def amend_message(ex, extra):
    msg = '{} with context: {}'.format(ex.args[0], extra) if ex.args else extra
    ex.args = (msg,) + ex.args[1:]
like image 20
jfs Avatar answered Oct 06 '22 06:10

jfs