6

I'm trying to override the printed output from an Exception subclass in Python after the exception has been raised and I'm having no luck getting my override to actually be called.

def str_override(self):
    """
    Override the output with a fixed string
    """

    return "Override!"

def reraise(exception):
    """
    Re-raise an exception and override its output
    """

    exception.__str__ = types.MethodType(str_override, exception, type(exception))

    # Re-raise and remove ourselves from the stack trace.
    raise exception, None, sys.exc_info()[-1]

def test():
    """
    Should output "Override!" Actually outputs "Bah Humbug"
    """
    try:
        try:
            raise Exception("Bah Humbug")
        except Exception, e:
            reraise(e, "Said Scrooge")
    except Exception, e:
        print e

Any idea why this doesn't actually override the str method? Introspecting the instance variables shows that the method is actually overridden with the method but it's like Python just refuses to call it through print.

What am I missing here?

James
  • 3,852
  • 2
  • 19
  • 14

2 Answers2

13

The problem is not that __str__() doesn't get overriden (just like you've already said, it does), but rather that str(e) (which invisibly gets called by print) is not always equivalent to e.__str__(). More specifically, if I get it right, str() (and other special methods, such as repr()), won't look for str in the instance dictionary - it would only look for it in the class dictionary. At least that is the case for the so-called new-style classes (which are the only classes in Python 3.x IIRC). You can read more about it here:

http://mail.python.org/pipermail/python-bugs-list/2005-December/031438.html

If you want to change the exception error message for a reraised exception, you can do something like this instead:

def reraise(exception):
    """
    Re-raise an exception and override its output
    """

    exType = type(exception)
    newExType = type(exType.__name__ + "_Override", (exType,), { '__str__': str_override})
    exception.__class__ = newExType

    # Re-raise and remove ourselves from the stack trace.
    raise exception, None, sys.exc_info()[-1]

This will dynamically derive a new exception class with the str override, and change exception to be an instance of that class. Now your code should work.

user48956
  • 14,850
  • 19
  • 93
  • 154
Boaz Yaniv
  • 6,334
  • 21
  • 30
  • Actually, on closer inspection it won't: it changes the class of exception. As I mentioned below, I'm basically changing the exception message of a deep class and I need to maintain its outward behaviour. Changing the class means I have to change any code that handles this exception and I don't want to do that. – James May 07 '11 at 20:36
  • Actually...maybe it will. *grin* Sorry for the back and forth: there's some interesting subtlies in your answer. If I understand the code correctly, you're creating a dynamic SUBCLASS which should still keep the original exception interface to the outside world. Ie// raise A; reraise B(A); catch A: should catch instances of B too. Slick! – James May 07 '11 at 20:41
  • 1
    @James: Yep, that was the idea behind it. :) Subclasses of `A` should always be caught by `catch A:`. – Boaz Yaniv May 07 '11 at 20:43
  • Like I said: slick! Thanks. – James May 07 '11 at 20:48
  • This is awesome. I had never thought about dynamically deriving types this way before. Thanks for the post. – Matt Jan 23 '14 at 14:30
2

http://docs.python.org/reference/datamodel.html#special-method-lookup-for-new-style-classes states that "For new-style classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary". IOW, you can't just assign a method to some_instance.__str__. Also, Monkey Patching will not work on builtin types like exceptions. Which you wouldn't want anyway, not even for a non-builtin exception class, since that patch would change the behaviour of all instances of that class.

If you're feelin' kinda hackish you could instead do something like:

...
except DaUncoolException, e:
    e.args = ('cool override stuff!',) + e.args[1:]
    raise

I don't like this very much, though. Why would you want to do such a thing anyway?

pillmuncher
  • 10,094
  • 2
  • 35
  • 33
  • I've got an older class in a 500KSLOC program that's buried deep in the application and throws exceptions that are handled much higher up the stack. I want to append information to every exception that is thrown by that class. – James May 07 '11 at 17:32
  • Your solution seems to work (and keeps the exception class the same as what was thrown). Any notion how reliable appending to args is? Is it safe to rely on most printing the contents of args somewhere in the message? – James May 07 '11 at 20:38
  • If the class of `e` is a subclass of `exception.BaseException` and nobody messed with the internals then it should work. But beware that in Python 2.x any old style class can be raised as an exception, even if it does not subclass a builtin exception class. Those will probably not have the `args` attribute. – pillmuncher May 07 '11 at 20:57