12

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]
3
  • Inside the except block ex.args = (key,)+ex.args is a bit cleaner? Commented Jul 16, 2013 at 13:35
  • @SteveAllison: You could do it that way, but the message will be presented as a tuple, such as ZeroDivisionError: ('0: ', 'division by zero'). Commented Jul 16, 2013 at 13:38
  • More modern answers over at stackoverflow.com/a/75549200/674039 Commented Jul 22, 2024 at 16:38

2 Answers 2

6

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
Sign up to request clarification or add additional context in comments.

1 Comment

That's certainly cleaner than my initial attempt. As verbose as it is, it does the simple job of changing the message. Thanks.
2

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:]

3 Comments

Thanks J.F. I did not think about chaining exception to resolve my issue. The problem with that solution is that the default python exception handler displays both stack traces on top of each other, so one has to do a little bit of scrolling (when the stack is large) to understand the context. I am really hoping to just amend the message.
@elmotec: I've added "encapsulate in a function" solution.
Falls back to pretty much the same as @unubtu. Unfortunately, I can only pick one answer, so I pick this one because it shows both changing ex.args and exception chaining. Thanks both.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.