-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rust style force handling of return value #6936
Comments
In principle I like this idea, although it is probably relatively low priority. If you want this to end up in |
This is similar to #2499, although the proposed implementation mechanism here is different. |
Not relevant for the issue, just wanted to say you can get closer to Rust with something like the following: result.pyfrom typing import Generic, TypeVar, Union
from typing_extensions import Final, final
T = TypeVar('T')
E = TypeVar('E')
@final
class Ok(Generic[T]):
def __init__(self, value: T) -> None:
self.value: Final = value
def is_error(self) -> bool:
return False
@final
class Err(Generic[E]):
def __init__(self, error: E) -> None:
self.error: Final = error
def is_error(self) -> bool:
return True
Result = Union[Ok[T], Err[E]] Example: def divide(a: float, b: float) -> Result[float, ZeroDivisionError]:
if b == 0.0:
return Err(ZeroDivisionError())
return Ok(a / b)
result = divide(10, 0)
print(result.is_error())
if isinstance(result, Ok):
# reveal_type(result.value) -> float
print(f'Ok({result.value}')
elif isinstance(result, Err):
# reveal_type(result.error) -> ZeroDivisionError
print(f'Err({result.error!r})')
else:
# For exhaustiveness checking, see https://github.com/python/mypy/issues/5818.
assert_never(result) |
@JelleZijlstra Yeah, this may be one of the possible ways to move towards #2499 |
Here is another random idea about handling errors using return values with more minimal syntax (at the cost of requiring some special-purpose primitives): from mypy_extensions import Result, is_error
# Result would be magic alias almost like this:
# T = TypeVar('T')
# E = TypeVar('E', bound=Exception)
# Result = Union[T, E]
def divide(a: float, b: float) -> Result[float, ZeroDivisionError]:
if b == 0:
return ZeroDivisionError()
return a / b
def foo() -> Result[None, RuntimeError]:
result = divide(10, 0)
if is_error(result):
# Type of result is 'ZeroDivisionError' here
return RuntimeError()
# Type of result is 'float' here
print(result + 1)
In Python 3.8 def foo() -> Result[None, RuntimeError]:
if is_error(result := divide(10, 0)):
return RuntimeError()
# Type of result is 'float' here
print(result + 1) |
@JukkaL This would be neat. One problem that comes up is, what if |
It would have to be wrapped in another object, something like |
One thing IMO worth noting, once you introduce monadic types you start having some oddities with e.g. callbacks, where the given and expected return types don't necessarily match, or a lot of utility unwrapping functions. Languages built around monads (e.g. Haskell) don't really have this type of issue because the style of the language itself blends with monads really well. Maybe what Swift does would be worth a look? If what we really want is checked error handling that's not as verbose as Java's Of course I just have to plug my old error handling article at the same time. 😂 I think Swift largely did better than my idea did, but if we're really looking at return values it's not half bad. |
@refi64 I once thought about borrowing the error handling mechanism from Swift. I don't remember if I wrote anything about this. Here is another idea inspired by Swift and @refi64's article: from mypy_extensions import Error, check
def divide(a: float, b: float) -> float | Error:
if b == 0:
# Inferred to raise ZeroDivisionError due to explicit "raise"
raise ZeroDivisionError()
return a / b # No exception inferred from this
def foo() -> None | Error:
result = check(divide(10, 0)) # 'check' required to propagate exception
print(result + 1)
def bar() -> None | Error:
try:
result = divide(10, 0) # 'check' not required inside 'try'
except ZeroDivisionError
raise RuntimeError()
print(result + 1)
def foobar() -> None:
try:
result = divide(10, 0) # 'check' not required inside 'try'
except ZeroDivisionError
sys.exit(1) # No checked exception from exit()
print(result + 1) # No checked exception from print()
# foo may raise checked ZeroDivisionError
# bar may raise checked RuntimeError
# foobar doesn't raise a checked exception Random notes:
|
Honestly that looks amazing, though I feel like the semantics around this being, or at least looking like, a return value seem to be a bit confusing. E.g. def spam() -> int | Error:
# ...
def eggs() -> None:
# Is this valid
x = spam()
check(x)
# but then how might this work?
try:
y = x
# ... Given the overall style of the proposal, maybe something simply like As for the stuff like RuntimeError, maybe declare that only Exception subclasses are checked, not all Error subclasses? That way stuff like SystemExit and AssertionError don't make things weird. |
No,
Yeah, that is an option, but it wouldn't interact as nicely with
Not sure how we'd represent this using the decorator. Also, in some real-world code the majority of functions might raise unchecked exceptions, and the
Most built-in exception classes, including |
Hmm true, I understand that, it just seems kinda odd that it looks like a return type but doesn't act one.
Ugh right, this is what I get for writing comments on bug reports around midnight... |
I would be happy to volunteer to annotate a real project to test this. I don’t have any significant open source projects but I wouldn’t mind testing this on my internal code and sharing it with a few individuals under NDA as a test bed for this idea. Personally I feel like all subclasses of Exception should be checked, as missing them leads to unexpected application errors. My experience with IndexError in particular is that I rarely want that to bubble out of a function because I lose sight of its meaning. I will usually convert it into a more meaningful exception if I need it to travel up further. With regards to syntax, I like raises better than the pipe proposal even though it doesn’t jive as well with Callable. My proposal with Callable would be to add an optional 3rd element to it, like so: FooFunc = Callable[[],None,Union[IndexError,IOError]] I would even propose a step farther and propose something like the following for readability (I don’t know if Callable already supports kwargs): FooFunc = Callable[ As far as @raises goes, multiple types could be passed as *args but for a nod towards Callable maybe it would be better to require this: @raises ( Union[IndexError,IOError] ) I had another idea about how we could solve this is create the ability to apply a Callable to a function definition, like so: def foo(): # type: FooFunc This disadvantage here is it makes it harder to read a function’s signature in context but it’s less work to change function signatures. I’m not convinced this is a net win but I thought I’d throw it out there anyway. [Edited because autocorrect sucks] |
Would someone please advise: How could I go about putting a bounty on this feature? |
I don't think we support anything like this, but maybe others know @gvanrossum @JukkaL |
No, there are no bounties here. |
I really like the monadic way or error handling as demonstrated by https://github.com/dry-python/returns. As someone else already noted this requires some machinery for wrapping, unwrapping and composing functions written in this style. The upside of having helper functions like |
Only a tiny number of people are going to adopt that style. It looks quite unpythonic to me. |
@gvanrossum I agree with your sentiment when applied to the python community as a whole; when only applied to the part of the community that uses mypy things might look different. The challenge is making a feature like this look and feel like python. |
Not really, mypy is commonly used to annotate large existing code bases.
|
I believe this would be a great idea as someone who just wants to write safer code. I found the project https://github.com/dry-python/returns some time ago and loved the idea, but didn't use it because of the same reason @gvanrossum mentioned is that it's not very pythonic. And felt that those using my code and not familiar with that My question as just an average developer is, until something like this is worked out in mypy or even a stand alone package, how would you guys recommend trying to at least start on something like this for a new code base? I like the syntax proposed in the comment #6936 (comment) with I agree in a way that I guess my question is what could be a recommended pattern if I wanted to try to wrap my return values in one of the methods described here, or to have this style of error checking, in a way that might later worth with mypy. I know the method isn't decided yet but if done cleanly enough, migrating to the accepted pattern when/if mypy implements this or supports it, shouldn't be too bad with good testing in place. My favorite is the ability for a mypy setting when using these, to enforce that we always handle the Ok/Err whenever it is used. Edit: Another library that is trying to do a |
I just want to point out one use case is Important Save a reference to the result of this function, to avoid a task disappearing mid-execution. The event loop only keeps weak references to tasks. A task that isn’t referenced elsewhere may get garbage collected at any time, even before it’s done. For reliable “fire-and-forget” background tasks, gather them in a collection: |
I'm not sure if something like |
There are tons of functions that should work this way; in fact since error handling is typically done via exceptions in python requiring return values to be used should probably be the default for everything, it's almost always a bug in python when you ignore a return. The occasional thing that has a frequently unused return value should be manually opted out with some kind of annotation like |
I tried to implement something like Rust's Result type. The interesting thing is that it has a linting attribute called #[must_use] which makes the linter enforce that the value is not ignored when used as a return type.
I think this could be very helpful when implementing functions that returns status and you want to ensure that it is not unhandled.
Consider the following toy implementation:
The text was updated successfully, but these errors were encountered: