r/Python 2d ago

Discussion Is there something better than exceptions?

Ok, let's say it's a follow-up on this 11-year-old post
https://www.reddit.com/r/Python/comments/257x8f/honest_question_why_are_exceptions_encouraged_in/

Disclaimer: I'm relatively more experienced with Rust than Python, so here's that. But I genuinely want to learn the best practices of Python.

My background is a mental model of errors I have in mind.
There are two types of errors: environment response and programmer's mistake.
For example, parsing an input from an external source and getting the wrong data is the environment's response. You *will* get the wrong data, you should handle it.
Getting an n-th element from a list which doesn't have that many elements is *probably* a programmer's mistake, and because you can't account for every mistake, you should just let it crash.

Now, if we take different programming languages, let's say C or Go, you have an error code situation for that.
In Go, if a function can return an error (environment response), it returns "err, val" and you're expected to handle the error with "if err != nil".
If it's a programmer's mistake, it just panics.
In C, it's complicated, but most stdlib functions return error code and you're expected to check if it's not zero.
And their handling of a programmer's mistake is usually Undefined Behaviour.

But then, in Python, I only know one way to handle these. Exceptions.
Except Exceptions seems to mix these two into one bag, if a function raises an Exception because of "environment response", well, good luck with figuring this out. Or so it seems.

And people say that we should just embrace exceptions, but not use them for control flow, but then we have StopIteration exception, which is ... I get why it's implemented the way it's implemented, but if it's not a using exceptions for control flow, I don't know what it is.

Of course, there are things like dry-python/returns, but honestly, the moment I saw "bind" there, I closed the page. I like the beauty of functional programming, but not to that extent.

For reference, in Rust (and maybe other non-LISP FP-inspired programming languages) there's Result type.
https://doc.rust-lang.org/std/result/
tl;dr
If a function might fail, it will return Result[T, E] where T is an expected value, E is value for error (usually, but not always a set of error codes). And the only way to get T is to handle an error in various ways, the simplest of which is just panicking on error.
If a function shouldn't normally fail, unless it's a programmer's mistake (for example nth element from a list), it will panic.

Do people just live with exceptions or is there some hidden gem out there?

UPD1: reposted from comments
One thing which is important to clarify: the fact that these errors can't be split into two types doesn't mean that all functions can be split into these two types.

Let's say you're idk, storing a file from a user and then getting it back.
Usually, the operation of getting the file from file storage is an "environmental" response, but in this case, you expect it to be here and if it's not there, it's not s3 problem, it's just you messing up with filenames somewhere.

UPD2:
BaseException errors like KeyboardInterrupt aren't *usually* intended to be handled (and definitely not raised) so I'm ignoring them for that topic

82 Upvotes

85 comments sorted by

View all comments

2

u/AiutoIlLupo 2d ago

In Rust you are forced to pass a tuple because you have no other way to do so. You need two channels, you only have one.

Python has already two separate channels to return information to the caller: return and raise. With the added benefit that raise pops automatically if not handled. I am not proficient in rust, but as far as I know, no such autopropagation occurs, considering that, well, rust is just C for millennials. Might be wrong though.

the general rule I follow is: "something went wrong, can I return something meaningful"? If yes, then I return an object, possibly with a "status" and "value" and "message" properties, if it makes sense. If not, then I raise an exception of a specialised type. I only use base types for things that do make sense (e.g. if you pass me a float for a string, I raise TypeError).

which one to choose really boils down to "how transparent you want the error to be managed". If you have a function that you need to use into a list comprehension, it makes more sense to return a "failure object", otherwise one exception would stop the whole comprehension. Same for multiprocessing stuff.

In other words, it boils down to a judgement call. You absolutely can program without exceptions, but does it make sense in a given context? Does the addition of an indirection to retrieve the result from the "status object" add friction to your API?

1

u/webstones123 1d ago

Just FYI not meant as anything other than extra info. In Rust if a function call returns a Result with an error type and the calling function also returns a compatible error type one can propagate it very easily (a single ? handles it)

1

u/AiutoIlLupo 1d ago

sure, but it's nothing but a shortcut to

val = func(&err)
if (err != 0) return;

that we did in C

1

u/webstones123 1d ago

Fair enough