What is the idiomatic python way to hide traceback errors unless a verbose or debug flag is set?

Example code:

their_md5 = 'c38f03d2b7160f891fc36ec776ca4685'
my_md5 = 'c64e53bbb108a1c65e31eb4d1bb8e3b7' 
if their_md5 != my_md5:
    raise ValueError('md5 sum does not match!')

Existing output now, but only desired when called with foo.py --debug:

Traceback (most recent call last):
  File "b:\code\apt\apt.py", line 1647, in <module>
    __main__.__dict__[command] (packages)
  File "b:\code\apt\apt.py", line 399, in md5
    raise ValueError('md5 sum does not match!')
ValueError: md5 sum does not match!

Desired normal output:

ValueError: md5 sum does not match!

Here’s a test script: https://gist.github.com/maphew/e3a75c147cca98019cd8

The short way is using the sys module and use this command:

sys.tracebacklimit = 0

Use your flag to determine the behaviour.

Example:

>>> import sys
>>> sys.tracebacklimit=0
>>> int('a')
ValueError: invalid literal for int() with base 10: 'a'

The nicer way is to use and exception hook:

def exception_handler(exception_type, exception, traceback):
    # All your trace are belong to us!
    # your format
    print "%s: %s" % (exception_type.__name__, exception)

sys.excepthook = exception_handler

Edit:

If you still need the option of falling back to the original hook:

def exception_handler(exception_type, exception, traceback, debug_hook=sys.excepthook):
    if _your_debug_flag_here:
        debug_hook(exception_type, exception, traceback)
    else:
        print "%s: %s" % (exception_type.__name__, exception)

Now you can pass a debug hook to the handler, but you’ll most likely want to always use the one originated in sys.excepthook (so pass nothing in debug_hook). Python binds default arguments once in definition time (common pitfall…) which makes this always work with the same original handler, before replaced.

try:
    pass # Your code here
except Exception as e:
    if debug:
        raise # re-raise the exception
              # traceback gets printed
    else:
        print("{}: {}".format(type(e).__name__, e))

I’ll answer wickedly:

Don’t hide tracebacks

Traceback is to show developer an error – unhandled exception. Something that delelopers didn’t prepare, didn’t notice. In one word – abomination.

But you are the clever one. You see that there are cases when md5 sum will not much, and than you shall end your program. So maybe let’s do exaclty that:

if their_md5 != my_md5:
  sys.stderr.write('md5 sum does not match!')
  exit(-1)

What is more – your fellow devs would be thankfull – they will still have their tracebacks and will never try to handle such exception.
Shortly – there is no reason to raise exception if you don’t want to handle it.

But…

if you really have to raise exception (so maybe there will be some cases that would be acceptable, and your fellow dev would like to handle it, e.g. with sha256, or it would be small part of gigantic project) than you can do this:

 def do_sth_with_md5(their_md5, my_md5):
     if their_md5 != my_md5:
         raise ValueError('md5 sum does not match!')
     actual_staff_to_do(their_md5, my_md5)
    (...)

 ... somewhere else ...
 try:
     do_sth_with_md5(their, my)
 except ValueError:
     sys.stderr.write('md5 sum does not match!') #or sha256handling... whatever
     exit(-1)

     

Of course those are simplified examples…

Use the logging system to handle the error output.

I’ll use requests connection error as an example use case, it generates a cascade of 3 exceptions each with a lengthy traceback. Only the final error message is of real importance – the called service is refusing, I don’t need to know 3 pages of traceback from my client app!

  1. Define a custom error class
# file: custom_exceptions.py

class Error(Exception):
    """This class should be used where traceback should not be logged"""
    pass
  1. In the inner function, catch the exception and cast it to the custom error class
    try:
        response = requests.get(url, auth=auth, headers=headers, timeout=TIMEOUT)
    except requests.exceptions.ConnectionError as e:
        raise custom_exceptions.Error(e)
  1. Handle the custom exception and unexpected exceptions differently in the caller function
    except custom_exceptions.Error as e:    # any custom error based on the custom Error class
        if LOGLEVEL=logging.DEBUG:
            logger.exception(e)     # with traceback
        else:
            logger.error(e)         # just the exception and message
        exit()
    except Exception as e:
        logger.exception(e)         # with traceback
        exit()

The resultant log message – all the detail you need for this error scenario:

2021-07-23 10:58:27,545 [ERROR] HTTPConnectionPool(host="localhost", port=8080): Max retries exceeded with url: /next (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7fc54f11f9d0>: Failed to establish a new connection: [Errno 111] Connection refused'))