DEV Community

Discussion on: Python exceptions considered an anti-pattern

Collapse
 
jonathanhiggs profile image
Jonathan Higgs

I'm torn by this. While I do like the railway programming model a lot, and the the idea of returning additional information makes a lot of sense in cases of potential failure (C# for example has bool TryGet<TKey, TValue>(TKey key, out TValue value) to deal with lookup failure without exceptions), I feel that Exceptions are highly useful and don't really suffer from the issues you assign to them

They aren't difficult to notice, when they happen I get a big stack trace dump and my IDE helpfully breaks when they are thrown. I'd say they are much more difficult to know ahead of time, but the cause of this is that python is impossible to statically analyse, it is a language problem and not an Exception problem. Other languages have tried noting functions are noexcep or listing all the possible exceptions that might come from a function call, but ultimately those either didn't solve the know-ability problem or created too much overhead to be useful.

Program flow is as unclear as without using them. There is always the issue of not knowing how a function is called, and therefore where the program might continue on an error. The only defense we have against that is to write clear and specific code, SOLID etc.

Comparisons to goto are extremely unjustified. It is just as easy to say that function calls, and even if statements are just structured gotos. The exact same reasoning could be applied to those but we still use them because it would be impossible to do anything without them. Since a raw goto can make it very difficult to follow a program, we have specific sub-cases where they make sense and restrict their use as much as possible to those sub-cases. It is absolutely possible to make a program difficult to follow with misuse of exceptions, it is equally possible to make it difficult to follow with poorly designed functions

Exceptions are a control-flow mechanism. Not all functions can produce a result when the accept variable input. Sometimes remote resources aren't available. Sometimes cosmic rays hit a register and flip a bit leading to undesired behavior. The places where some undesired state is hit is probably not the place that needs to decide how to continue with execution. Exceptions allow us to control this unexpected state and make that decision at the appropriate point in the control flow

Again, I really like the railway notion for high level program flow, but down in the weeds of making things happen that syntax is far too obscure and much less readable

Collapse
 
sobolevn profile image
Nikita Sobolev • Edited

Wow, thanks for this reply. It is awesome.

I agree that it is easy to notice exceptions when they happen. It is hard to notice them ahead of time. There are a lot of different attempts to solve it, including checked exceptions in java.

Program flow is as unclear as without using them

I cannot agree. The main difference is that Result is just a regular value. It returns to the same place where the function was originally called. While exceptions can jump to any other layer of your call stack. And it will depend on the execution context. That's what I have called "two execution flows" in the article.

Comparisons to goto are extremely unjustified

Well, it seems rather similar to me:

try:
    print(1 / 0)
except ZeroDivisionError:
    do_something()
Enter fullscreen mode Exit fullscreen mode

It is clear that except ZeroDivisionError is going to catch this exception.
Now, we will move this print(1 / 0) into a new function called print_value():

def print_value():
    print(1 / 0)
Enter fullscreen mode Exit fullscreen mode

But, this except ZeroDivisionError will still execute. And it looks like goto mark to me.

I really like the railway notion for high level program flow

That's exactly the case! That's why I have this "Limitations" section. I prefer my business-logic to be safe and typed, while my application might throw exceptions that are common to the python world. Frameworks will handle them.

Collapse
 
yawpitch profile image
Michael Morehouse

But, this except ZeroDivisionError will still execute. And it looks like goto mark to me.

But that's your apparent misunderstanding... it doesn't behave at all like a goto ... like not even within the same ballpark.

This:

try:
    return n / d
except ZeroDivisionError:
    return foo()
Enter fullscreen mode Exit fullscreen mode

Is fundamentally no different than:

if not d:
    return foo()
return n / d
Enter fullscreen mode Exit fullscreen mode

And adding in a deeper level of function nesting doesn't change that ... this:

def div(n, d):
    return n / d

try:
    return div(n, d)
except ZeroDivisionError:
    return foo()
Enter fullscreen mode Exit fullscreen mode

Is now fundamentally no different than:

if not d:
    return foo()
return div(n, d)
Enter fullscreen mode Exit fullscreen mode

The exception handler is effectively just a convenient way of writing the boilerplate for whatever test(s) would be required in that if to satisfy any given exception state, with the added benefit of deferring the test until an actual exception state has been encountered and needs to be addressed (or allowed to bubble further up the stack).

If you're writing the exception handler there's no magic (and certainly nothing that resembles a goto; you're reacting to a specific exception that has happened below you and the traceback tells you precisely where that came from ... if your reaction is to call a function then you know precisely where the control flow goes to and also that it will always return to you, even if it returns to you in the form of another exception (like for instance a SystemExit you've wrapped in the response function). A goto is a completely arbitrary escape from normal control flow, an exception is not, it's entirely predictable, though I'll happily admit it's not always easy to predict.

If no exception handling is involved then the behaviour is very predictable: where the exception is raised the stack frame exits and bubbles up a level. In each successively higher frame in which it's not handled it's effectively the same as catching it and immediately re-raising it, and so it continues to bubble ... when it hits the surface the interpreter catches it, prints the traceback, and shuts down in an orderly fashion with a non-zero returncode.

I don't have an issue with you wanting to make error handling a bit more sane for yourself, especially if you're going to try to enforce type constraints, but comparison to goto is just incorrect.

Collapse
 
fakuivan profile image
fakuivan • Edited

Exceptions totally do behave like gotos, a clear example is how you can use exceptions to "short circuit" things like map or functions that take in other functions. Here's an example:

def find_first_zero_pos(ints: list[int]) -> int | None:
    class FoundZero(Exception):
        pass
    i = 0
    def inc_or_rise_if_zero(num: int):
        if num == 0:
            raise FoundZero()
        nonlocal i
        i += 1

    try:
        for _ in map(inc_or_rise_if_zero, ints):
            pass
    except FoundZero:
        return i
    return None
Enter fullscreen mode Exit fullscreen mode

In this case, execution flow is returned to find_first_zero_pos from map at will. You would not be able to do this with a language that does not support exceptions using only pure code (a function that shuts down the computer doesn't count lol). The with statement exists in part to be able to handle this "execution could end at any moment" situation by providing a way to do cleanup even when an exception is thrown.

The problem with goto is that usually execution flow always goes down into a function and comes up out of it, aka your call stack is actually a stack. gotos allow you to never return from a function, jumping to another section of code while your call stack is not a stack anymore. Python even has a way to type this, it's the NoReturn return type for functions, which is automatically assigned by static types analizers to functions that raise unconditionally.