A simple Result type for Python 3inspired by Rust,fully type annotated.
Latest release:
$ pip install result
Latest GitHubmain
branch version:
$ pip install git+https://github /rustedpy/result
The idea is that a result value can be eitherOk(value)
or
Err(error)
,with a way to differentiate between the two.Ok
and
Err
are both classes encapsulating an arbitrary value.Result[T, E]
is a generic type alias fortyping.Union[Ok[T], Err[E]]
.It will
change code like this:
defget_user_by_email(email:str)->Tuple[Optional[User],Optional[str]]:
"""
Return the user instance or an error message.
"""
ifnotuser_exists(email):
returnNone,'User does not exist'
ifnotuser_active(email):
returnNone,'User is inactive'
user=get_user(email)
returnuser,None
user,reason=get_user_by_email('ueli@example ')
ifuserisNone:
raiseRuntimeError('Could not fetch user: %s'%reason)
else:
do_something(user)
To something like this:
fromresultimportOk,Err,Result,is_ok,is_err
defget_user_by_email(email:str)->Result[User,str]:
"""
Return the user instance or an error message.
"""
ifnotuser_exists(email):
returnErr('User does not exist')
ifnotuser_active(email):
returnErr('User is inactive')
user=get_user(email)
returnOk(user)
user_result=get_user_by_email(email)
ifis_ok(user_result):
# type(user_result.ok_value) == User
do_something(user_result.ok_value)
else:
# type(user_result.err_value) == str
raiseRuntimeError('Could not fetch user: %s'%user_result.err_value)
Note that.ok_value
exists only on an instance ofOk
and
.err_value
exists only on an instance ofErr
.
And if you're using Python version3.10
or later, you can use the
elegantmatch
statement as well:
fromresultimportResult,Ok,Err
defdivide(a:int,b:int)->Result[int,str]:
ifb==0:
returnErr("Cannot divide by zero")
returnOk(a//b)
values=[(10,0), (10,5)]
fora,binvalues:
matchdivide(a,b):
caseOk(value):
print(f "{a}//{b}=={value}")
caseErr(e):
print(e)
Not all methods (https://doc.rust-lang.org/std/result/enum.Result.html) have been implemented, only the ones that make sense in the Python context. All of this in a package allowing easier handling of values that can be OK or not, without resorting to custom exceptions.
Auto generated API docs are also available at ./docs/README.md.
Creating an instance:
>>>fromresultimportOk,Err
>>>res1=Ok('yay')
>>>res2=Err('nay')
Checking whether a result isOk
orErr
:
ifis_err(result):
raiseRuntimeError(result.err_value)
do_something(result.ok_value)
or
ifis_ok(result):
do_something(result.ok_value)
else:
raiseRuntimeError(result.err_value)
Alternatively,isinstance
can be used (interchangeably to type guard functions
is_ok
andis_err
). However, relying onisinstance
may result in code that
is slightly less readable and less concise:
ifisinstance(result,Err):
raiseRuntimeError(result.err_value)
do_something(result.ok_value)
You can also check if an object isOk
orErr
by using theOkErr
type. Please note that this type is designed purely for convenience, and
should not be used for anything else. Using(Ok, Err)
also works fine:
>>>res1=Ok('yay')
>>>res2=Err('nay')
>>>isinstance(res1,OkErr)
True
>>>isinstance(res2,OkErr)
True
>>>isinstance(1,OkErr)
False
>>>isinstance(res1,(Ok,Err))
True
Convert aResult
to the value orNone
:
>>>res1=Ok('yay')
>>>res2=Err('nay')
>>>res1.ok()
'yay'
>>>res2.ok()
None
Convert aResult
to the error orNone
:
>>>res1=Ok('yay')
>>>res2=Err('nay')
>>>res1.err()
None
>>>res2.err()
'nay'
Access the value directly, without any other checks:
>>>res1=Ok('yay')
>>>res2=Err('nay')
>>>res1.ok_value
'yay'
>>>res2.err_value
'nay'
Note that this is a property, you cannot assign to it. Results are immutable.
When the value inside is irrelevant, we suggest usingNone
or a
bool
,but you're free to use any value you think works best. An
instance of aResult
(Ok
orErr
) must always contain something. If
you're looking for a type that might contain a value you may be
interested in amaybe.
Theunwrap
method returns the value ifOk
andunwrap_err
method
returns the error value ifErr
,otherwise it raises anUnwrapError
:
>>>res1=Ok('yay')
>>>res2=Err('nay')
>>>res1.unwrap()
'yay'
>>>res2.unwrap()
Traceback(mostrecentcalllast):
File"<stdin>",line1,in<module>
File"C:\project\result\result.py ",line107,inunwrap
returnself.expect("Called `Result.unwrap()` on an `Err` value")
File"C:\project\result\result.py ",line101,inexpect
raiseUnwrapError(message)
result.result.UnwrapError:Called`Result.unwrap()`onan`Err`value
>>>res1.unwrap_err()
Traceback(mostrecentcalllast):
...
>>>res2.unwrap_err()
'nay'
A custom error message can be displayed instead by usingexpect
and
expect_err
:
>>>res1=Ok('yay')
>>>res2=Err('nay')
>>>res1.expect('not ok')
'yay'
>>>res2.expect('not ok')
Traceback(mostrecentcalllast):
File"<stdin>",line1,in<module>
File"C:\project\result\result.py ",line101,inexpect
raiseUnwrapError(message)
result.result.UnwrapError:notok
>>>res1.expect_err('not err')
Traceback(mostrecentcalllast):
...
>>>res2.expect_err('not err')
'nay'
A default value can be returned instead by usingunwrap_or
or
unwrap_or_else
:
>>>res1=Ok('yay')
>>>res2=Err('nay')
>>>res1.unwrap_or('default')
'yay'
>>>res2.unwrap_or('default')
'default'
>>>res1.unwrap_or_else(str.upper)
'yay'
>>>res2.unwrap_or_else(str.upper)
'NAY'
Theunwrap
method will raised anUnwrapError
.A custom exception can
be raised by using theunwrap_or_raise
method instead:
>>>res1=Ok('yay')
>>>res2=Err('nay')
>>>res1.unwrap_or_raise(ValueError)
'yay'
>>>res2.unwrap_or_raise(ValueError)
ValueError:nay
Values and errors can be mapped usingmap
,map_or
,map_or_else
and
map_err
:
>>>Ok(1).map(lambdax:x+1)
Ok(2)
>>>Err('nay').map(lambdax:x+1)
Err('nay')
>>>Ok(1).map_or(-1,lambdax:x+1)
2
>>>Err(1).map_or(-1,lambdax:x+1)
-1
>>>Ok(1).map_or_else(lambda:3,lambdax:x+1)
2
>>>Err('nay').map_or_else(lambda:3,lambdax:x+1)
3
>>>Ok(1).map_err(lambdax:x+1)
Ok(1)
>>>Err(1).map_err(lambdax:x+1)
Err(2)
To save memory, both theOk
andErr
classes are ‘slotted’, i.e. they
define__slots__
.This means assigning arbitrary attributes to
instances will raiseAttributeError
.
Theas_result()
decorator can be used to quickly turn ‘normal’
functions intoResult
returning ones by specifying one or more
exception types:
@as_result(ValueError,IndexError)
deff(value:int)->int:
ifvalue==0:
raiseValueError# becomes Err
elifvalue==1:
raiseIndexError# becomes Err
elifvalue==2:
raiseKeyError# raises Exception
else:
returnvalue# becomes Ok
res=f(0)# Err[ValueError()]
res=f(1)# Err[IndexError()]
res=f(2)# raises KeyError
res=f(3)# Ok[3]
Exception
(or evenBaseException
) can be specified to create a
‘catch all’Result
return type. This is effectively the same astry
followed byexcept Exception
,which is not considered good practice in
most scenarios, and hence this requires explicit opt-in.
Sinceas_result
is a regular decorator, it can be used to wrap
existing functions (also from other libraries), albeit with a slightly
unconventional syntax (without the usual@
):
importthird_party
x=third_party.do_something(...)# could raise; who knows?
safe_do_something=as_result(Exception)(third_party.do_something)
res=safe_do_something(...)# Ok(...) or Err(...)
ifis_ok(res):
print(res.ok_value)
Do notation is syntactic sugar for a sequence ofand_then()
calls.
Much like the equivalent in Rust or Haskell, but with different syntax.
Instead ofx <- Ok(1)
we writefor x in Ok(1)
.Since the syntax is
generator-based, the final result must be the first line, not the last.
final_result:Result[int,str]=do(
Ok(x+y)
forxinOk(1)
foryinOk(2)
)
Note that if you exclude the type annotation,
final_result: Result[float, int] =...
,your type checker may be
unable to infer the return type. To avoid an errors or warnings from
your type checker, you should add a type hint when using thedo
function.
This is similar to Rust'sm! macro:
usedo_notation::m;
letr =m!{
x <-Some(1);
y <-Some(2);
Some(x + y)
};
Note that if your do statement has multiplefor`s, you can access an identifier bound in a previous `for.Example:
my_result:Result[int,str]=do(
f(x,y,z)
forxinget_x()
foryincalculate_y_from_x(x)
forzincalculate_z_from_x_y(x,y)
)
You can usedo()
with awaited values as follows:
asyncdefprocess_data(data)->Result[int,str]:
res1=awaitget_result_1(data)
res2=awaitget_result_2(data)
returndo(
Ok(x+y)
forxinres1
foryinres2
)
However, if you want to await something inside the expression, use
do_async()
:
asyncdefprocess_data(data)->Result[int,str]:
returndo_async(
Ok(x+y)
forxinawaitget_result_1(data)
foryinawaitget_result_2(data)
)
Troubleshootingdo()
calls:
TypeError("Got async_generator but expected generator")
Sometimes regulardo()
can handle async values, but this error means
you have hit a case where it does not. You should usedo_async()
here
instead.
These steps should work on any Unix-based system (Linux, macOS, etc) with Python
andmake
installed. On Windows, you will need to refer to the Python
documentation (linked below) and reference theMakefile
for commands to run
from the non-unix shell you're using on Windows.
- Setup and activate a virtual environment. SeePython docsfor more information about virtual environments and setup.
- Run
make install
to install dependencies - Switch to a new git branch and make your changes
- Test your changes:
make test
make lint
- You can also start a Python REPL and import
result
- Update documentation
- Edit any relevant docstrings, markdown files
- Run
make docs
- Add an entry to thechangelog
- Git commit all your changes and create a new PR.
- Why should I use the
is_ok
(is_err
) type guard function over theis_ok
(is_err
) method?
As you can see in the following example, MyPy can only narrow the type correctly while using the type guardfunctions:
result:Result[int,str]
ifis_ok(result):
reveal_type(result)# "result.result.Ok[builtins.int]"
else:
reveal_type(result)# "result.result.Err[builtins.str]"
ifresult.is_ok():
reveal_type(result)# "Union[result.result.Ok[builtins.int], result.result.Err[builtins.str]]"
else:
reveal_type(result)# "Union[result.result.Ok[builtins.int], result.result.Err[builtins.str]]"
- Why do I get the "Cannot infer type argument" error with MyPy?
There isa bug in MyPy
which can be triggered in some scenarios. Usingif isinstance(res, Ok)
instead ofif res.is_ok()
will help in some cases. Otherwise using
one of these
workarounds
can help.
- dry- Python /returns: Make your functions return something meaningful, typed, and safe!
- alexandermalyga/poltergeist: Rust-like error handling in Python, with type-safety in mind.
MIT License