0

It is bad practice to not capture exceptions of an inner function and instead do it when calling the outer function? Let us look at two examples:

Option a)

def foo(a, b):
    return a / b

def bar(a):
    return foo(a, 0)

try:
    bar(6)
except ZeroDivisionError:
    print("Error!")

Pro: cleaner (in my opinion)
Con: you cannot tell which exceptions bar is raising without looking at foo

Option b)

def foo(a, b):
    return a / b

def bar(a):
    try:
        ret = foo(a, 0)
    except ZeroDivisionError:
        raise

    return ret

try:
    bar(6)
except ZeroDivisionError:
    print("Error!")

Pro: explicit
Con: you are just writing a try-except block that re-raises the exception. Also ugly, in my opinion

Other options?

I understand that if you want to do something with the exception or group several exceptions together option b is the only choice. But what if you only want to re-raise some specific exceptions as is?

I could not find anything in the PEP that sheds some light into this.

skd
  • 1,865
  • 1
  • 21
  • 29
  • Why in option b do you want to re-raise? Just by calling bar it will get raised in any case – Karl Sep 02 '20 at 07:52
  • @Karl that is exactly my question, if it is OK to write option a. The problem is that you do not inmediatly know what exceptions bar can raise by looking at the code. So a possible argument for re-raising is to be explicit. – skd Sep 02 '20 at 07:54
  • I'd go with option a. If you want to know where the exception was originated from you can always look at the stack trace – Tomer Sep 02 '20 at 08:01
  • The best in my opinion is if you handle the exception directly in the function where it can occur (i.e. within "bar"). I.e. as in option b), but without re-raising again why you call "bar(6)". The problem with option b) is that you would have to build the execption handling every time you call the function. Better if it handles its exceptions itself. – Karl Sep 02 '20 at 08:05
  • sorry, last sentence should read "the problem with option a)" – Karl Sep 02 '20 at 08:14
  • @Karl I agree with you. The only problem is that there are times when a function can't directly handle all its exceptions and you end up with code like this, so I'm interested in hearing what people think. Seems option a) is winning so far. Also I'm looking for any pointers to Python official style guides – skd Sep 02 '20 at 09:13
  • In my view if you encounter unhandled exceptions you should go back to your function and handle them there. If you don't have control over the function (i.e. using an external package) then you should additionally wrap them – Karl Sep 02 '20 at 09:24

2 Answers2

1

If you go by the Clean Code book by Uncle Bob, you should always separate logic and error handling. This would make the option a.) the preferred solution.

I personally like to name functions like this:

def _foo(a, b):
    return a / b

def try_foo(a, b):
    try:
        return _foo(a, b)
    except ZeroDivisionError:
        print('Error')


if __name__ == '__main__':
    try_foo(5, 0)        
tilman151
  • 563
  • 1
  • 10
  • 20
1

Dealing with Errors

Is it a bad practice? To my opinion: No, it's not. In general this is GOOD practice:

def foo(a, b):
    return a / b

def bar(a):
    return foo(a, 0)

try:
    bar(6)
except ZeroDivisionError:
    print("Error!")

The reason is simple: Code dealing with the error is concentrated at a single point in your main program.

In some programming languages exceptions that could potentially be raised must be declared on function/method level. Python is different: It is a script language that lacks features like this. Of course therefore you might get an exception quite unexpectedly at some times as you might not be aware that other code you're invoking could raise such an exception. But that is no big deal: To resolve that situation you have the try...except... in your main program.

You can compensate for this lack of knowledge about possible exceptions as follows:

  • document exceptions that could be raised; if the programming language does not help here by itself, you need to make up for this deficit by providing a more extensive documentation;
  • perform extensive tests;

In general it makes no sense at all to follow your option b). Things might be more explicit but the code itself is not the right place for this explicit information. Instead this information should be part of your function's/method's documentation.

Therefore instead of ...

def bar(a):
    try:
        ret = foo(a, 0)
    except ZeroDivisionError:
        raise

    return ret

... write:

def bar(a):
    """
    Might raise ZeroDivisionError
    """
    return foo(a, 0)

Or as I would write it:

#
# @throws   ZeroDivisionError       Does not work with zeros.
#
def bar(a):
    return foo(a, 0)

(But which syntax you exactly rely on for documentation is a completely different matter and beyond the scope of this question.)

There are situations when catching exceptions within a function/method are a good practice. For example this is the case if you want a method to succeed in any way even if some internal operation might fail. (E.g. if you try to read a file and if it does not exist you want to use default data.) But catching an exception just in order to raise it again typically does not make any sense: Right now I can't even come up with a situation where this might be useful (though there might be some special cases). If you want to provide information that such an exception could be raised, do not rely on users looking into the implementation but rather into the documentation of your function/method.

Outputting Errors

In any way I would not follow your approach of just printing a simple error message:

try:
    bar(6)
except ZeroDivisionError:
    print("Error!")

It is quite labor-intensive to come up with reasonable, human readable, simple error messages. I used to do this but the amount of code you need for that approach is immense. To my experience it is better to just fail and print out the stack trace. With this stack trace typically anyone can find the reason for the error very easily.

Unfortunately Python does not provide a very readable stack trace in error output. To compensate for this I implemented my own error output handling (reusable as a module) that even makes use of colors, but that's a different matter and might be a bit beyond the scope of this question as well.

Regis May
  • 3,070
  • 2
  • 30
  • 51