Tuesday, November 5, 2013

Pattern of Error Reporting in Python

Instead of Intro


That is not an uncommon and even not rare when an application which was supposed to be a prototype suddenly becomes a tool for everyday usage. Not a perfect thing here is that in a rush to make the program more or less user-friendly a developer has to hide internals by preserving error messages with internal details. Here I am going to share the pattern I mostly use in my "so-called-prototype" Python scripts; especially when they are translated to binaries using py2exe.

The pattern allows to introduce an extended error reporting in Python scripts w/o any extra costs.

Own Errors


The only rule I follow when there is a need to raise an error is to forget about standard ready-to-use Python exceptions. Due the following reasons:

  • Need to distinguish where an error came from: from the "Batteries" or from the application's logic;
  • Need to express an application's domain in the code.
The rule is true even for a spaghetti-style code which is supposed to be thrown away tomorrow or even today; this will cost nothing but might help with debugging.

So introduce own exception class:

class Error(Exception):
    def __init__(self, message, innerError = None):
        msg = message
        if innerError:
            msg += " *- {0}".format(str(innerError))

        Exception.__init__(self, msg)

The exception class here is straight forward for the sake of simplicity. In serious applications it is much better to introduce a field for an inner error, environment etc.

Respect Each Error


When a script's function pass flow control to another one there is a bell that the flow goes to another virtual layer. Each new layer is worth to have own logged error if there is any.

Suppose below that foo() is a first layer, bar() is a second one. So the application might look like:

def tryRussianRoulette():
    ### NOTE: that is a very bad practice to put import statements somewhere in a logic
    import random
    
    isFired = (0 == random.randint(0, 5))
    if isFired:
        raise Error("bang!")
        
def bar():
    try:
        tryRussianRoulette()
    except Error, e:
        raise Error("Russian Rouletter has fired", e)

def foo():
    try:
        bar()
    except Error, e:
        raise Error("Failed to bar-bar", e)

def main():
    foo()

def propagateToUser(error):
    if isinstance(error, Error):
        print "[Error]", str(e)
    else:
        print "[Unknown error]", str(e)

if "__main__" == __name__:   
    try:
        main()
    except Exception, e:
        propagateToUser(e)
        sys.exit(1)

    sys.exit(0)


tryRussianRoulette() is a function which may cause an error.
When application is accidentally in a production, an error for end-user (!) would look like:

[Error] Failed to bar-bar *- Russian Rouletter has fired *- bang!

In most cases (if you have not skipped error handling on each layer), the error is descriptive enough to understand the problem.

Pattern in Action


How to introduce an ability for an extended error tracing w/o writing tons of extra code? The answer is to vary try/except statement's behavior depending on system's environment variable bound to an application.

The application's code above is just extended with the function:

def isDebug():
    withLettersOnly = lambda string: filter(lambda ch: ch.isalpha(), string)
    appName = os.path.basename(sys.argv[0])
    debugKey = "{appName}_DEBUG".format(appName = withLettersOnly(appName))

    return os.environ.get(debugKey, False)

and try/except statement will be replaced with the following code:

    try:
        main()
    except Exception, e:
        if isDebug():
            raise
        else:
            propagateToUser(e)
            sys.exit(1)

If the newly developed application is run from a file named bing-bang.py, environment variable bingbangpy_DEBUG set to a non-empty value will cause a raw Python's stack trace instead user-friendly error. The similar is true for a Python's script compiled using py2exe; guess a bound environment variable name.

Instead of Summary


  1. The pattern code has been intentionally left primitive for one reason: to allow your to play around and find a suitable implementation;
  2. Introduced own exception class could contain locals() and globals() of a corresponding layer; or system details. That totally depends on your fantasy;
  3. The pattern works well for small scripts; and for "proof-of-concepts" applications which might be used in real-life until RTM. Avoid the approach in the case of more or less serious applications.


No comments:

Post a Comment