Live data from Hacker News

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

inngest.com

31–40 of 77 posts

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

#31
post #11

The most important thing about writing code is that you write it idiomatically for the language it is in. With Python, idiomatic code is known as Pythonic. def rename_user(user_id: str, name: str) -> User | Exception: # Consume the function user = get_user(user_id) if isinstance(user, Exception): return user user.name = name return user This is not Pythonic. Don't do it. Like it or not, Python uses exceptions. How do…

lol the most important thing about writing code is that it works, and works well enough that it puts shekels in my pocket. I'll start worrying about Pythonic code when my accountant does. Until then, we'll keep counting shekels.

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

#32
> So if we want to be really safe then we'll wrap each call with a try/catch:

    try:
        thing.set_name("Doodad")
    except Exception as err:
        raise Exception(f"failed to set name: {err}") from err
> As we think about each possible error we realize that our original logic would crash the program when we didn't want it to! But while this is safe it's also extremely verbose.

How is this safer than the original?

If the caller wasn't catching exceptions thrown by this function before, it's not catching exceptions thrown by this function now. What is being gained by catching an exception to do nothing but throw another exception?

This feels like a strawman.

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

#33
post #10

I'm six months into my Python journey. We aren't building a library, so everything runs on 3.11. Having spent most of my career in statically typed and sometimes functional languages, I've found the result package approach and pattern-matching suggestion work well. There's been a suggestion it's not very Pythonic, but I'm willing to continue using a result monad because the trade-off is one-sided; it comfortably pays…

What is a result monad?

It just lets you combine results in an intuitive way:

    Ok(2) + Ok(3) = Ok(5)

    Ok(2) + Err() = Err()

    Err() + Ok(3) = Err()

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

#35
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.

Sure, it depends on what is expensive. For us, in a primarily asynchronous domain, saving cycles is helpful for lower computational costs and the environment, but in the main, we optimise for reading the code, and one way to lower the cognitive load is to avoid using errors to control flow.

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

#36

Earlier quoted context omitted.

Author here! > Why does the author want to swallow these exceptions? Sometimes you want to swallow exceptions and sometimes you don't. The examples in the article may be a little contrived, but there are situations where logging an error and continuing is better because it prevents data loss. > Let them propagate and it's obvious where the issue lies. If they propagate down the stack then you lose context. You also m…

> If they propagate down the stack then you lose context. You also may not have all the data you need to properly recover (e.g. still write to a table but make the errored field null). Is this not solved by using raise from?

By "context" I mean other data that lived at the time of the error. For example, you may want to update a table row when there's an error but if a thrown error takes you too far down the stack then you might not know the row ID anymore

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

#37
Nope, don't like it. It's also a little disingenuous to switch the example halfway through. Anyway, I fixed your code, with exceptions

    def get_user(user_id: str) -> User:
        rows = users.find(user_id=user_id)
        if not rows:
            raise Exception("user not found")
        return rows[0]

    def rename_user(user_id: str, name: str) -> User:
        user = get_user(user_id)
        user.name = name
        return user, None

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

#39

python is where I learned to hate exceptions as control flow - Twisted is twisted. Go was such a breath of fresh air. Now I'm back in python primarily and I am constantly wondering what my functions actually take and actually return. Exceptions are just spooky GOTO and a distance. Our logs are littered with them and have to use Sentry to tell us "oops, you introduced a new error path." Our builds are full of warnings…

> Exceptions are just spooky GOTO and a distance I couldn't agree more! When you throw exceptions it's unclear where the control flow will go

I just don't see how multiple levels of

    if err:
        return nil, err
in the call stack makes things any clearer? Or how having many instances of that snippet scattered everywhere makes the code easier to read?

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

#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 a try/catch around about 20 lines of code, with superficial logging/rethrowing and no context about exactly what was being done to what when the exception occurred, just a filename/line# and a probably cryptic error message.

In Go, best practice is something like:

    bytes, err := os.ReadFile("myfile.json")
    if err != nil {
        return nil, fmt.Errorf("reading file %s: %v", "myfile.json", err)
    }

    var data map[string]any
    err = json.Unmarshal(bytes, &data)
    if err != nil {
        return fmt.Errorf("unpacking json from file %s: %v", "myfile.json", err)
    }
This gives you precisely targeted errors that tell exactly what you were doing to what. Your future self will thank you when you're desperately trying to work out what went wrong last thing on a Friday.

If you are going to replicate this with exceptions, it would require much more boilerplate, as his example demonstrates, which is ironic given that is the charge levelled at Go.

Post reply on HN