PEP 484 introduced type hints, at this time documenting exceptions was left to docstrings. I seek to suggest a reason this feature might be desirable along with how it might be used. Error handling in python does an excellent job of keeping the error-path out of the way when writing the normal flow of logic, however for larger code bases it is not always clear what exceptions may be caused by calling existing code. Since these cases are easily missed they may reach a higher level than intended ...
This is a discussion on Python’s forums about adding something akin to a throws keyword in python.
I disagree, I’d instead like to move toward handling errors as logic, and keeping exceptions for actually exceptional cases. If you’re expecting an exception, that’s data.
So here’s my proposal:
introduce monads like Maybe/Result that forces the dev to handle expected errors in logic
make an easy way to return errors early without interrupting logic flow
simplify checking for None values in chaining
For the first (not exactly a monad, may need a new type to wrap things):
def maybe_err(val:int) ->Result[int, ValueError]:ifval< 0:return ValueError("cannot be negative")
returnvalmatch (val:= maybe_err(-1)):
case int():
case ValueError():
For the second:
val= maybe_error(-1)? # special handling toreturn instances ofError early
And the third:
val = x?.y?.z ?? DEFAULT
I like this much better than having try/except blocks throughout the code, and reserve those only for logging and whatnot at the top level. If you document exceptions, people will use them even more as data instead of exceptions.
So only raise if you want it to bubble all the way up, return errors if it’s just data for the caller. Libraries should almost never raise.
I’m not too familiar with C# (last used it like a decade ago), but I think the rules here would be pretty simple:
x? - if x is None or an Error, return from the function early, otherwise use the value and continue
x?.y - same as above, but with an attribute of x
x ?? y - instead of returning as in the first, use y as the default value
And maybe add an option to convert exceptions from a function to an Error value (maybe some_func?() to convert to error values? IDK, I haven’t thought through that part as much).
Hopefully that’s simple enough to be useful.
If I were proposing this, I’d limit it to optional chaining since that’s far more annoying to me currently.
@sugar_in_your_tea If you’re expecting exceptions, make custom ones. That’s the best way to distinguish between those you expect and those you don’t. Using custom exceptions improves readability too.
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.
@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.
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 {
letval= some_func_that_can_error()?;
returnSome(val.operation_that_can_error());
}
letval=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.
@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?
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.
@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.
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.
I disagree, I’d instead like to move toward handling errors as logic, and keeping exceptions for actually exceptional cases. If you’re expecting an exception, that’s data.
So here’s my proposal:
For the first (not exactly a monad, may need a new type to wrap things):
def maybe_err(val: int) -> Result[int, ValueError]: if val < 0: return ValueError("cannot be negative") return val match (val := maybe_err(-1)): case int(): case ValueError():
For the second:
val = maybe_error(-1)? # special handling to return instances of Error early
And the third:
val = x?.y?.z ?? DEFAULT
I like this much better than having try/except blocks throughout the code, and reserve those only for logging and whatnot at the top level. If you document exceptions, people will use them even more as data instead of exceptions.
So only raise if you want it to bubble all the way up, return errors if it’s just data for the caller. Libraries should almost never raise.
Anything but over9000 variations of nullables like in C#
I’m not too familiar with C# (last used it like a decade ago), but I think the rules here would be pretty simple:
And maybe add an option to convert exceptions from a function to an Error value (maybe
some_func?()
to convert to error values? IDK, I haven’t thought through that part as much).Hopefully that’s simple enough to be useful.
If I were proposing this, I’d limit it to optional chaining since that’s far more annoying to me currently.
@sugar_in_your_tea If you’re expecting exceptions, make custom ones. That’s the best way to distinguish between those you expect and those you don’t. Using custom exceptions improves readability too.
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:
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.
@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.
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.
@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?
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.
@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.
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.
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.