2

So lately I've been asking a few questions about more professional and pythonic style in Python, and despite being given great answers to my questions, I feel like I need to ask a much broader question.

In general, when writing utility functions (for a library, etc.) that deal more with side effects (file writes, dictionary definitions, etc.) than return values, it's very useful to return a status code to tell the calling function that it passed or failed.

In Python, there seem to be three ways to flag this:

Using a return value of -1 or 0 (C like) and using statements such as

if my_function(args) < 0:
    fail condition
pass condition

or using a return value of True/False

if not my_function(args):
     fail condition
pass condition

or using a 'return or 'return None' using exceptions (exits on unknown error)

try:
    my_function(args)
except ExpectedOrKnownExceptionOrError:
    fail condition
pass condition

Which of these is best? Most correct? Preferred? I understand all work, and there isn't much technical advantage of one over the other (except perhaps the overhead of exception handling).

Philip Massey
  • 1,401
  • 3
  • 14
  • 24

3 Answers3

7

Don't return something to indicate an error. Throw an exception. Definitely don't catch an exception and turn it into a return code.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Yup, this is EAFP approach, very common in Python. C-like approach is probably worst here, but in the end, it all depends on the exact goal of the library, and what is more consistent in terms of common usage. – Tadeck Jul 11 '13 at 17:02
  • Ok, so here's my question. If my arguments fail to meet the function requirements, should I just throw a basic Exception? Like if data < 0: raise Exception('bad input') – Philip Massey Jul 11 '13 at 17:09
  • 2
    Throw the appropriate type of exception. Most often, that's `TypeError` if the types are wrong or `ValueError` if the types are right but an argument's value somehow doesn't fit (for example, a sequence longer than it should have been). The usual way to throw such an error is to start your function by doing something that will throw the error for you if the arguments are wrong - for example, unpacking an argument that's supposed to be a sequence, or doing math with something that should be a number. – user2357112 Jul 11 '13 at 17:14
2

When an exception is raised and not caught, Python will exit with an error (non-zero) code for you. If you are defining your own exception types, you should override sys.excepthook() to provide you with the exit codes you desire, since they will by default use the exit code 1.

If you want to specify your exit code for some weird reason, use sys.exit() and the errno module to get the standard exit codes so that you will use the appropriate one. You can also use the traceback module to get the stack traceback (and it seems the correct error code as well, according to that answer). Personally, I don't prefer this approach either.

The approach I recommend is to not catch the exception; just let it happen. If your program can continue to operate after an exception occurs, you ought to catch it and handle it appropriately. However, if your program cannot continue, you should let the exception be. This is particularly useful for re-using your program in other Python modules, because you will be able to catch the exception if you so desire then.

Community
  • 1
  • 1
2rs2ts
  • 10,662
  • 10
  • 51
  • 95
1

Much like everything else: It depends.

Exceptions are good for "passing the buck" up the chain for errors or conditions the local routine (or its caller -- see below) just can't handle or be expected to handle. Especially if "whoever" can handle it is located several levels up the call stack. I consider this the "optimal" use of exceptions.

The downside of exceptions is that once an exception is raised, there's no going back to pick up where you left off -- even if the problem got fixed. You've got to re-start the routine again from the beginning.

Return values are good for routine actions that are not expected to fail "unexpectedly". Check the return value and go from there. They are also useful for problems the local routine can recover from. For example, if the routine expects to try something 3 times before giving up, a return Pass/Fail/True/False is likely appropriate.

In my experience, expecting the caller to handle both return values and exceptions from its callee just gets tedious. In my opinion, checking return values inside a Try/Except block just makes clumsy logic... at least for the work I do.

This is not to say that you can't use exceptions instead of return values. There are definitely situations where that can provide cleaner code.

My compromise has been to try to limit myself to return values or exceptions within a single routine. I try to limit the things my caller needs to check.

When I need both, I try to limit my use exceptions to problems my direct caller can't handle either. In other words my direct caller will expect to get a True/False/Whatever return value, but is not expected to handle exceptions as well. Exceptions would be handled perhaps by their caller, etc. When using both in the same routine, I try to limit my exceptions to the truly "exceptional" (i.e. uncommon problems).

Also, the preferred Python idiom is to use True/False instead of 1/0. Unless of course you're writing in C or you're returning a Linux exit code in your Linux scripts.

JS.
  • 14,781
  • 13
  • 63
  • 75