jnosal

On programming and stuff

Not so exceptional anymore

| Comments

I'm going to share my personal view on some cases, connected more or less with python exception handling.

1. Worst python antipattern revisited

Everyone knows this is harmful:

try:
    <code>
except:
    pass

but, this (not so harmful in someone's eyes) snippet is even worse imho:

try:
    <code>
except as e:
    logging.error(e)

We may assume that someone was accessing complex function, which internals he was not familiar with that much, so he decided to log any exception that occur. And unless it wasn't quick script or snippet, that should tell us at least three things about this person:

a) he probably didn't bother writing unit tests
b) he didn't care enough to identify function internals and special cases that may occur (ValueErrors, KeyErrors etc)
c) he knows about the first anti-pattern (except / pass), but is lazy enough to actually reproduce it, just slightly improving on quality

and unless you're using sentry or somewhat similar software and rely purely on log files, I'm almost 100% sure that most of the exception will be lost in the abyss.

2. Ignoring with ignored

Introduced (to me) by Raymond Hettinger useful pattern to silencing unwanted exceptions - use with caution

@contextmanager
def ignored(*exceptions):
    try:
        yield
    except exceptions:
        pass
with ignored(ZeroDivisionError):
    print "Where is your God now ?"
    2 / 0

3. Using what's already been done

Unless you're writing big or very generic library or some fancy project I'd suggest you rely on already defined exceptions, which are present in Standard Library

a) Raise TypeError: use when someone's tries to play with your business logic in a harmful way - e.g providing car instances to function that expects bank accounts to do some money operations.

b) Raise AttributeError: may be used instead common ConfigurationExceptions that are thrown whenever something is not present in configuration / setting files

c) Raise KeyError: whenever handler is missing for specific actions, like so:

def play_music(device, *args, **kwargs):
    handlers = {
        'radio': radio_handler,
        'computer': computer_handler,
    }
    
    if action not in handlers:
        raise KeyError(u"Handler definition missing for {0}".format(action))
    
    handler = handlers[device]
    handler(*args, **kwargs)

d) Raise ValueError: throw anytime when data provided is invalid and doesn't match expectations, for instance You expect number between 1 and 100 and someone enters 1000

This little gang of four should keep you going for a while ;-)

4. Don't raise NotImplementedError

I literally hate snippets like this:

class BaseNotificationGateway(object):
    
    def prepare(self):
        raise NotImplementedError
        
    def notify(self):
        raise NotImplementedError

class IAmNotDefinedButIWillNotFailYet(BaseNotificationGateway):
    pass
    
gateway = IAmNotDefinedButIWillNotFailYet()

# later

gateway.notify()

It's time abc module becomes more popular - it's much better and flexible for creating interfaces.

import abc

class BaseNotificationGateway(object):
    __metaclass__ = abc.ABCMeta
    
    @abc.abstractmethod
    def prepare(self):
        pass
    
    @abc.abstractmethod    
    def notify(self):
        pass
        

class IAmMissingSomething(BaseNotificationGateway):
    pass
    
# will raise TypeError on instance creation

gateway = IAmMissingSomething()

It's not the best idea to put pass in each function perhaps, but this pattern is extremely useful for application factories: http://flask.pocoo.org/docs/0.10/patterns/appfactories/, which may be used to setup different objects that may be used later on app startup.

5. Drop try except else finally statements and favour context managers

Instead of this:

from app.services import github

client = github.ApiClient()

try:
    response = client.fetch()
except github.SuperCustomException as e:
    # Possibly log exception

    # Fallback action

else:
    # Handle response

finally:
    # Close what's to close

    # Cleanup

put it in context manager and let your API be neat and beautiful

from app.services import github

with github.ApiClient() as client:
    response = client.fetch()
    # handle response

6. Know your library

Django, Sqlalchemy, requests -they all come with create deal of predefined exceptions: non existing rows, http errors, timeouts, validation errors - it's all there for you.

Enjoy!

Comments

comments powered by Disqus