Python – User-defined exception:

exceptionpythonpython-2.7

I tried to define my own exception class in python 2.7, deriving from BaseException.

class NestedCommentException(BaseException):
    """
    Exception for nested comments
    """
    def __init__(self, file_path, list_lines):
        self.file_path = file_path
        self.list_lines = list_lines

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        return 'File {0} contains nested comments at lines {1}'.format(self.file_path, ', '.join(self.list_lines))

But when throwing it, it cannot be printed: raise NestedCommentException(file_path, list_lines) triggers

Traceback (most recent call last):
  File "D:\DATA\FP12210\My Documents\Outils\SVN\05_impl\2_tools\svn_tag_setup.py", line 85, in <module>
    tag_checks()
  File "D:\DATA\FP12210\My Documents\Outils\SVN\05_impl\2_tools\svn_tag_setup.py", line 66, in tag_checks
    check_nested_comments(ddl_path)
  File "D:\DATA\FP12210\My Documents\Outils\SVN\05_impl\2_tools\svn_tag_setup.py", line 54, in check_nested_comments
    raise NestedCommentException(file_path, list_lines)
NestedCommentException: <unprintable NestedCommentException object>

Can you please explain why this happens, even if I defined __str__ and __repr__ methods ?

Best Answer

TL;DR

When you see this thing, it basically means that some kind of exception has been raised in __str__() of your object. So unless the problem is trivial enough to see at the first sight (e.g. forgotten "%s"), either

  • wrap the __str__ body in a try/except clause as Anurag advices, or

  • instantiate your exception and call the __str__ (or any methods you may have overridden) manually, outside the traceback module, so that you get the full description of the exception.

Analysis

Actually this <unprintable MyException object> can come from various functions in traceback module, which, when trying to get a string (i.e. "printable") version of a value (exception), it

  1. calls str() on it, and if anything goes wrong,

  2. tries to treat it as unicode and convert it to ASCII, and if still anything goes wrong

  3. simply prints the above representation.

Responsible code (same in 2.6 and 2.7):

def _some_str(value):
    try:
        return str(value)
    except Exception:
        pass
    try:
        value = unicode(value)
        return value.encode("ascii", "backslashreplace")
    except Exception:
        pass
    return '<unprintable %s object>' % type(value).__name__

As you can see, any exceptions coming from the str() call or the unicode.encode() call are ceased in the process and only the "cryptic" representation is given.

Note on traceback module vs. Python interpreter

Contrary to what traceback documentation tells us:

It exactly mimics the behavior of the Python interpreter when it prints a stack trace.

the representation given by Python interpreter is slightly different here. As opposed to the "unprintable" message, the interpreter would simply display the name of the exception, ceasing any actual exceptions as well.

Here is a simple script that demonstrates all three approaches: leaving the exception to Python interpreter, using traceback module, or calling the function by hand.

#!/usr/bin/python

import sys, traceback

class Boom(Exception):

    def __init__(self, foo, bar, baz):
        self.foo, self.bar, self.baz = foo, bar, baz

    def __str__(self):
        return ("boom! foo: %s, bar: %s, baz: "     # ouch! forgot an %s!
                % (self.foo, self.bar, self.baz))

def goBoom(): raise Boom(foo='FOO', bar='BAR', baz='BAZ')

if __name__ == "__main__":

    if sys.argv[1].startswith("i"):
        goBoom()
        # __main__.Boom

    elif sys.argv[1].startswith("t"):
        try:    goBoom()
        except: traceback.print_exc(file=sys.stdout)
        # Boom: <unprintable Boom object>

    elif sys.argv[1].startswith("m"):
        e = Boom(foo='FOO', bar='BAR', baz='BAZ')
        e.__str__()
        # TypeError: not all arguments converted during string formatting

    else: pass