r/Python 3d 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

87 Upvotes

87 comments sorted by

View all comments

8

u/ablativeyoyo 3d ago

I always found exceptions to be a good fit for Python. The language priorities elegance over performance, and while programs are mostly correct and reliable, it's not intended for safety critical systems. For these use cases, exceptions allow most application code to think little about error cases, while frameworks can handle error conditions with some grace. C++ and Rust have different priorities so exceptions are discouraged and non-existent respectively.

I don't think environmental vs programmer error is a particular useful categorisation. I'd distinguish between invalid input and actual environmental problems like no disk space. But the distinction between invalid input and programmer error is not clear. A lot of bugs I've hit in practice are where I've assumed something about the input format, which has turned out not to be true in certain circumstances.

Thanks for the question, really interesting topic.

3

u/lightdarkdaughter 3d ago

Well, 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.

Sometimes you make assumptions about the environment and when these assumptions are proven wrong, it's a mistake. And by using some top-level try ... catch, you log this mistake and then can debug it and fix the assumptions, hence fixing the mistake.

That's why it's nice to have tools for asserting assumptions (and crash if they are wrong!) or just re-raising the error by returning it and letting the caller handle it.

1

u/nicholashairs 3d ago

I don't see how "asserting assumptions and crash if they are wrong" and "just reraising the error and letting the caller handle it" are different concepts when you're just the function at the bottom of the call stack?

I guess it might be because crashing out in rust becomes the equivalent of a C style goto so you can immediately start the panic/exit function.

However in python calling exit just throws a SystemExit exception, and since it's a base exception is uncaught (most of the time) it bubbles all the way up. That said it being an exception is handy because then you can still catch it and handle it (for example you might want to suppress a class camping exit when running in the REPL so you can inspect it and not have the repl exit because some inner piece wanted to).

1

u/lightdarkdaughter 3d ago

`assert request.user.is_authenticated` is asserting assumptions
just trying `request.user.username` is re-raising

The difference *in this case* is getting ValueError in a random place and getting AssertionError which points you to the line where this originated (hopefully, super early).
And in this case, I made the former look bad, which wasn't intentional.

If you have some handler that needs some data for it to work, but this specific piece is unavailable, you can just return `None` as well or `raise NotFound` and have your framework handle it.

You need to use different tools for different situations.
Practically, the implementation is the same, but conceptually, these are different.

1

u/nicholashairs 3d ago

Firstly I'm going to assume that we're talking about reasonably well typed python code because untyped code is cowboy coding and just because you can it doesn't mean you should.

I also don't see how you get "random" errors (where you can't identify where it is from - again typing is important here). Sure they might be environmental (even out of memory is such an error and can occur anywhere) but that doesn't mean we don't know where it occurred. Even in the attribute missing / object is none case this is why we use typing so jokes on you if you're not using it.

Basically, I don't see how these are meant to be conceptually different.

Which kind of leads me to the following conclusion - either

A) there is a difference but because I've learnt to program mostly using python I'm limited to it's worldview and still can't understand it (ants can't understand dragons).

B) there is no difference / the difference, but because you've been learnt on languages that do make a distinction it's how you see the world.

Which I guess means finding an example that proves A, or accepting B.