A proposal for java's "throws" in python: Extend type hints to cover exceptions - eviltoast

This is a discussion on Python’s forums about adding something akin to a throws keyword in python.

  • sugar_in_your_tea@sh.itjust.works
    link
    fedilink
    arrow-up
    3
    ·
    1 year ago

    My point is that I don’t like using exceptions for communicating regular errors, only unrecoverable faults. So adding features to document exceptions better just doesn’t feel like the right direction.

    Maybe that’s un-Pythonic of me, idk. From the zen of Python:

    Errors should never pass silently.
    Unless explicitly silenced.

    Using monads could let programmers silently pass errors.

    I just really don’t like the exception model after years of using other languages (mostly Rust and Go), I much prefer to be forced to contend with errors as they happen instead of just bubbling them up by default.

    • twoframesperminute@mastodon.social
      link
      fedilink
      arrow-up
      0
      ·
      1 year ago

      @sugar_in_your_tea The idea of exceptions is that you can choose when to deal with them. So if you want to deal with them immediately,
      nothing is stopping you.

      If you think handling errors with every function call explicitly is easier, I guess you’re using very few functions. For the project I’m working on, your proposal would probably double the number of lines. Thanks, but no thanks.

      • sugar_in_your_tea@sh.itjust.works
        link
        fedilink
        arrow-up
        1
        ·
        edit-2
        1 year ago

        Handling can mean a lot of things. You can use a sigil to quickly return early from the function without cluttering up your code. For example, in Rust (code somewhat invalid because I couldn’t post the generic arg to Result because lemmy formatting rules):

        fn my_func() -> Result {
            let val = some_func_that_can_error()?;
            return Some(val.operation_that_can_error());
        }
        
        let val = match my_func() {
            Err(err) => {
                println!("Your error: {err}");
                return;
            }
            Some(val) => val,
        };
        // use val here
        

        That question mark inside my_func shows the programmer that there’s a potential error, but that the caller will handle it.

        I’m suggesting something similar for Python, where you can easily show that there’s a potential error in the code, without having to do much to deal with it when it happens if the only thing you want to do is bubble it up.

        If we use exceptions, it isn’t obvious where the errors could occur, and it’s easy to defer handling it much too late unless you want to clutter your code.

        • twoframesperminute@mastodon.social
          link
          fedilink
          arrow-up
          0
          ·
          1 year ago

          @sugar_in_your_tea I’m by far not qualified to discuss this in depth. But it seems to me that almost every function call ever can fail. Therefore, do you need to do this with every single function call?

          That seems terribly inefficient and bloated. How is that readable for anyone?

          • sugar_in_your_tea@sh.itjust.works
            link
            fedilink
            arrow-up
            1
            ·
            1 year ago

            That’s where the difference between exceptional cases comes in. Rust and Go both have the concept of a panic, which is an error that can only be caught with a special mechanism (not a try/except).

            So that’ll cover unexpected errors like divide by zero, out of memory, etc, and you’d handle other errors as data (e.g. record not found, validation error, etc).

            I don’t think Python should necessarily go as far as Go or Rust, just that handling errors like data should be an option instead of being forced to use try/except, which I find to be gross. In general, I want to use try/except if I want a stack trace, and error values when I don’t.

            • twoframesperminute@mastodon.social
              link
              fedilink
              arrow-up
              0
              ·
              edit-2
              1 year ago

              @sugar_in_your_tea But isn’t all that possible in Python? Don’t monads cover exactly what you want? Why does it need to be implemented some different way?

              Also, divide by zero should be data just as well. Failing to program around having nothing to divide by is not a reason to have a program panic.

              Also, having two systems for largely the same behavior doesn’t seem to improve usability and clarity, in my opinion.

              • sugar_in_your_tea@sh.itjust.works
                link
                fedilink
                arrow-up
                1
                ·
                1 year ago

                divide by zero should be data as well

                I disagree. You should be checking your input data so the divide by zero is impossible. An invalid input error is data and it can probably be recovered from, whereas a divide by zero is something your program should never do.

                If having the error is expected behavior (e.g. records/files can not exist, user data can be invalid, external service is down, etc), it’s data. If it’s a surprise, it’s an exception and should crash.

                doesn’t seem to improve usability

                I’m proposing that the programmer chooses. The whole design ethos around Python is that it should look like pseudocode. Pseudocode generally ignores errors, but if it doesn’t, it’s reasonable to express it as either an exception or data.

                Documenting functions with “throws” isn’t something I’d do in pseudocode because enumerating the ways something can fail generally isn’t interesting. However, knowing that a function call can fail is interesting, so I think error passing in the Rust way is an interesting, subtle way of doing that.

                I’m not saying we should absolutely go with monadic error returns, I’m saying that if we change error handling, I’d prefer to go that route than Java’s throws, because I think documenting exceptions encourages bad use of exceptions. The code I work on already has way too many try/except blocks, I’m concerned this would cement that practice.

                • twoframesperminute@mastodon.social
                  link
                  fedilink
                  arrow-up
                  0
                  ·
                  1 year ago

                  @sugar_in_your_tea Since when is Python supposed to equal pseudo code? It should be easily readable, but that doesn’t mean it should *equal* pseudo code.

                  You can either test for values being 0 before dividing, or catching an exception when it is. Especially when dividing multiple times in one function, I would go for the latter option.

                  • sugar_in_your_tea@sh.itjust.works
                    link
                    fedilink
                    arrow-up
                    1
                    ·
                    1 year ago

                    It’s not an explicit design goal, but it explains a lot of the Zen of Python and other pushback on PIPs, so to me it’s always been an unwritten design goal (be as close to pseudocode as practical, but no closer). It’s also how I generally write code (start with Python “pseudocode,” then decide what to use in production).

                    For example, from the Zen of Python:

                    There should be one-- and preferably only one --obvious way to do it.

                    Being clever in Python is a bad thing, just as it is in pseudocode. Python will never win awards for performance, so if you need that, you drop in something non-Python to do the expensive operations to keep the rest of the code clean and obvious.

                    If you think of Python as pseudocode, everything else makes a ton more sense.

                    You can test for values being 0 before dividing, or catching an exception when it is.

                    Ideally, you just test for input variables outside of the function and do neither. Something like:

                    def calc(x, y):
                        assert x > 0
                        assert y != 0
                        ...
                    

                    This throw exceptions if the preconditions fail, but those can (and should) be removed for production since their primary purpose is to inform the developer of the preconditions and catch mistakes in development. In production, you’d rely on some kind of schema validation to ensure the asserts never trigger (I’m partial to Pydantic).

                    So ideally you’d never expect a divide by zero or clutter your code with zero checks outside of those asserts (which shouldn’t be relied on) because you’ve already prevented those cases from happening.

                  • twoframesperminute@mastodon.social
                    link
                    fedilink
                    arrow-up
                    0
                    ·
                    1 year ago

                    @sugar_in_your_tea I don’t think we should change any functionality when it comes to exception handling. Code based documentation would be great for type checking and auto-generated docs, but they can be done using annotations, not changed interfaces.

                    Monads are already possible, but should not be the normal way to code either. It’s clunky and difficult to understand. It might work great for some scenarios, but doesn’t for many others.