Live data from Hacker News

Python errors as values: Comparing useful patterns from Rust and Go

inngest.com

61–70 of 77 posts

Re: Python errors as values: Comparing useful patterns from Rust and Go

#61
post #12
post #3

Oh, good, heavyweight error handling just in time for py3.11's zero-cost exception happy path. But, more generously: why not simply return an error, and use isinstance(val, Error) for error handling? Making objects and calling functions is quite costly, and that can largely be avoided.

This is what they ended the article on. Return a union type and then error check using isinstance.

Oh, so they did. I guess I got bored after reading so many bad approaches trying to write go/rust in python.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#62
post #42

I don't understand why people insist that all errors must be handled all the time. It could be my C++ background, but I feel there are two very different errors: Expected errors - like "user not found" - should use a value instead of exception. In Python, you can use sentinel objects, or tuples, or None.. lots of options really. Occasionally there is a good reason to use exceptions for flow control even for know erro…

> Expected errors - like "user not found" - should use a value instead of exception. In Python, you can use sentinel objects, or tuples, or None.. lots of options really.

That's why exceptions are called exceptions, not errors. If a routine called openFile() can't open a file, that's a pretty exceptional situation, and it's up to the caller to decide whether the exact reason is an error in their case.

The exception object is already a value that can have not only a message text, but also any data members, so why reinvent the wheel with sentinel objects, tuples, etc.?

> The examples in post, where you catch exception and re-raise Exception back are terrible - what's the point of them? There is no extra clarity, just verbosity. I would defect them in any code review.

Typically, you re-raise an exception after adding some content to it. This may be less important in Python, which gives you a great stack trace, but in a language like C++, the lack of context information makes the exception basically useless.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#63
post #47
post #40

Quite apart from the Python discussion, the author captures why I prefer errors as values (a la Go) to exceptions (a la Java), and I have written both styles for many years. > Regardless of the specific approach, returning errors as values makes us consider all of the places an error could occur. The error scenarios become self-documenting and more thoroughly handled. This is so true. Most Java exception handling is…

I think it's kinda the opposite actually... with go, you have to go out of the way to generate those errors, while in Python, most of this is automatic. In particular, the ReadFile example in Python will raise OSError, and those already include filename and error message. So you'd get the same result with 0 extra lines. For the second example, the json unmarshalling will not auto-add filename, but its easy enough to…

This is fine as far as it goes. The problem is that the exception can only report context that it knows about. By contrast, the Go version allows you to include any extra context you want, to make debugging easier. Eg, your error might not just include file reading or Json parsing, but which transaction or customer was involved at the time. You can do with exceptions but you either have to try/catch every statement, or add generic context in one big catch block.

I accept it's largely personal preference, but having used both mechanisms for many years, I find Go best practices for error handling are simple and easy to follow, and results in easily maintainable code, compared to exception handling which doesn't really come with a simple set of best practices, meaning it is often badly put together or added as an afterthought.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#64
post #49

Earlier quoted context omitted.

We write wrappers for standard and third-party libraries that throw errors. For example, we have our own dump_json function that catches-and-returns errors thrown by json.dumps. We didn't need many wrappers given the nature of our SDK, but some programs will need many wrappers and that could get unwieldy

But people are going to import your SDK, so your customers will be stuck with a mix of thrown exceptions and errors-as-values in their code. Based on your decision to write wrappers internally, presumably you'd argue that the sensible thing for them to do is to write wrappers either for your SDK or for every other library to get back to a consistent style and less mental overhead.

Author here!

We don't return errors in our public methods so our SDK still feels Pythonic. We're debating whether to add `_safe` suffix methods that return errors to give people the option, but for now our library only throws errors to consumers.

You might ask "well what's the point if you still throw errors to consumers?" We feel that forcing our engineers to deal with errors where they can happen is worthwhile. Our team writes a lot of Go and we love how it forces you to think about every spot that can error, so we wanted the same experience in Python

Re: Python errors as values: Comparing useful patterns from Rust and Go

#65

> tuple[User | None, Exception | None] tuple[User, None] | tuple[None, Exception]

Yup, don't allow nonsensical states (User+Exception or None+None) to even exist. The original is more like Go approach which is a big flaw with the language.

Go can be `tuple[User | None, Exception | None]` or `tuple[User, Exception | None]`, depending on whether you're returning a pointer. But yea, Go's approach has its warts. Like if you aren't returning a pointer then you need to return the zero value (e.g. `User{}`) even when returning an error

Re: Python errors as values: Comparing useful patterns from Rust and Go

#67
post #3

Oh, good, heavyweight error handling just in time for py3.11's zero-cost exception happy path. But, more generously: why not simply return an error, and use isinstance(val, Error) for error handling? Making objects and calling functions is quite costly, and that can largely be avoided.

> Oh, good, heavyweight error handling just in time for py3.11's zero-cost exception happy path. I don’t get it. Languages that use exceptions for all kinds of errors will also use exceptions for routine errors that happen as a matter of course—the happy path is not the overwhelmingly most common branch, and errors are not exceptional. In turn not zero-cost for all but the exceptional case.

The solution to this is to not use exceptions except for actual errors, unless it's going to be amortized away. For example, objects which have a heavy parameter, which gets memoized. With the zero-cost happy path, you only raise the exception once, when you're doing the costly thing, and subsequent accesses are free.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#68
post #49

Earlier quoted context omitted.

But people are going to import your SDK, so your customers will be stuck with a mix of thrown exceptions and errors-as-values in their code. Based on your decision to write wrappers internally, presumably you'd argue that the sensible thing for them to do is to write wrappers either for your SDK or for every other library to get back to a consistent style and less mental overhead.

Author here! We don't return errors in our public methods so our SDK still feels Pythonic. We're debating whether to add `_safe` suffix methods that return errors to give people the option, but for now our library only throws errors to consumers. You might ask "well what's the point if you still throw errors to consumers?" We feel that forcing our engineers to deal with errors where they can happen is worthwhile. Our…

OK, in that case I think it's a very reasonable choice! Perhaps it's worth clarifying that in the article.

I might express a slight preference that your internal code is idiomatic if I'm going to be poking around in there at some point. But I wouldn't be bikeshedding about your C coding style if you'd provided a Python module written in C, so I don't feel I should have a strong opinion as a user here.

Re: Python errors as values: Comparing useful patterns from Rust and Go

#69

"How we made a non-idiomatic Python SDK for our app that Python devs will hate"

Probably true. Without commenting on the particulars of the SDK in question (read: I haven't read TFA): Monadic code is obviously better than the mainstream alternatives. Does this point towards eventual inviability of Python itself?

Re: Python errors as values: Comparing useful patterns from Rust and Go

#70
post #63
post #47

Earlier quoted context omitted.

I think it's kinda the opposite actually... with go, you have to go out of the way to generate those errors, while in Python, most of this is automatic. In particular, the ReadFile example in Python will raise OSError, and those already include filename and error message. So you'd get the same result with 0 extra lines. For the second example, the json unmarshalling will not auto-add filename, but its easy enough to…

This is fine as far as it goes. The problem is that the exception can only report context that it knows about. By contrast, the Go version allows you to include any extra context you want, to make debugging easier. Eg, your error might not just include file reading or Json parsing, but which transaction or customer was involved at the time. You can do with exceptions but you either have to try/catch every statement,…

maybe there are some best error practices out there, but people don't follow that. As an example, I just went to github.com, got the top-trending go project ("ko"], search for error and arrived at this line in [0]:

    dtodf, err := os.Open(filepath.Join(filepath.Dir(file), "diffid-to-descriptor"))
    if err != nil {
      return nil, fmt.Errorf("opening diffid-to-descriptor: %w", err)
    }
See how they forgot to put filename in the error message? if there is some sort of error with the file, you'll have to resort to strace to find out what the name is... Not to mention that this very function returns json parse errors without context, and the caller "getMeta" calls multiple functions and returns the errors without context as well...

best practices are nice, but fully automatic is even better. The minimum-effort path in Python produced vastly more useful traces than in Go, and unfortunately too many programmers go mininum-effort path.

[0] https://github.com/ko-build/ko/blob/cfc13deeb6417d7e1582f031...

Post reply on HN