A simple Result type for Python 3 inspired by Rust, fully type annotated.
Latest release:
$ pip install result
Latest GitHub main
branch version:
$ pip install git+https://github.com/rustedpy/result
The idea is that a result value can be either Ok(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 for typing.Union[Ok[T], Err[E]]
. It will
change code like this:
def get_user_by_email(email: str) -> Tuple[Optional[User], Optional[str]]:
"""
Return the user instance or an error message.
"""
if not user_exists(email):
return None, 'User does not exist'
if not user_active(email):
return None, 'User is inactive'
user = get_user(email)
return user, None
user, reason = get_user_by_email('[email protected]')
if user is None:
raise RuntimeError('Could not fetch user: %s' % reason)
else:
do_something(user)
To something like this:
from result import Ok, Err, Result, is_ok, is_err
def get_user_by_email(email: str) -> Result[User, str]:
"""
Return the user instance or an error message.
"""
if not user_exists(email):
return Err('User does not exist')
if not user_active(email):
return Err('User is inactive')
user = get_user(email)
return Ok(user)
user_result = get_user_by_email(email)
if is_ok(user_result):
# type(user_result.ok_value) == User
do_something(user_result.ok_value)
else:
# type(user_result.err_value) == str
raise RuntimeError('Could not fetch user: %s' % user_result.err_value)
Note that .ok_value
exists only on an instance of Ok
and
.err_value
exists only on an instance of Err
.
And if you're using python version 3.10
or later, you can use the
elegant match
statement as well:
from result import Result, Ok, Err
def divide(a: int, b: int) -> Result[int, str]:
if b == 0:
return Err("Cannot divide by zero")
return Ok(a // b)
values = [(10, 0), (10, 5)]
for a, b in values:
match divide(a, b):
case Ok(value):
print(f"{a} // {b} == {value}")
case Err(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:
>>> from result import Ok, Err
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
Checking whether a result is Ok
or Err
:
if is_err(result):
raise RuntimeError(result.err_value)
do_something(result.ok_value)
or
if is_ok(result):
do_something(result.ok_value)
else:
raise RuntimeError(result.err_value)
Alternatively, isinstance
can be used (interchangeably to type guard functions
is_ok
and is_err
). However, relying on isinstance
may result in code that
is slightly less readable and less concise:
if isinstance(result, Err):
raise RuntimeError(result.err_value)
do_something(result.ok_value)
You can also check if an object is Ok
or Err
by using the OkErr
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 a Result
to the value or None
:
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.ok()
'yay'
>>> res2.ok()
None
Convert a Result
to the error or None
:
>>> 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 using None
or a
bool
, but you're free to use any value you think works best. An
instance of a Result
(Ok
or Err
) must always contain something. If
you're looking for a type that might contain a value you may be
interested in a maybe.
The unwrap
method returns the value if Ok
and unwrap_err
method
returns the error value if Err
, otherwise it raises an UnwrapError
:
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.unwrap()
'yay'
>>> res2.unwrap()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\project\result\result.py", line 107, in unwrap
return self.expect("Called `Result.unwrap()` on an `Err` value")
File "C:\project\result\result.py", line 101, in expect
raise UnwrapError(message)
result.result.UnwrapError: Called `Result.unwrap()` on an `Err` value
>>> res1.unwrap_err()
Traceback (most recent call last):
...
>>>res2.unwrap_err()
'nay'
A custom error message can be displayed instead by using expect
and
expect_err
:
>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> res1.expect('not ok')
'yay'
>>> res2.expect('not ok')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\project\result\result.py", line 101, in expect
raise UnwrapError(message)
result.result.UnwrapError: not ok
>>> res1.expect_err('not err')
Traceback (most recent call last):
...
>>> res2.expect_err('not err')
'nay'
A default value can be returned instead by using unwrap_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'
The unwrap
method will raised an UnwrapError
. A custom exception can
be raised by using the unwrap_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 using map
, map_or
, map_or_else
and
map_err
:
>>> Ok(1).map(lambda x: x + 1)
Ok(2)
>>> Err('nay').map(lambda x: x + 1)
Err('nay')
>>> Ok(1).map_or(-1, lambda x: x + 1)
2
>>> Err(1).map_or(-1, lambda x: x + 1)
-1
>>> Ok(1).map_or_else(lambda: 3, lambda x: x + 1)
2
>>> Err('nay').map_or_else(lambda: 3, lambda x: x + 1)
3
>>> Ok(1).map_err(lambda x: x + 1)
Ok(1)
>>> Err(1).map_err(lambda x: x + 1)
Err(2)
To save memory, both the Ok
and Err
classes are ‘slotted’, i.e. they
define __slots__
. This means assigning arbitrary attributes to
instances will raise AttributeError
.
The as_result()
decorator can be used to quickly turn ‘normal’
functions into Result
returning ones by specifying one or more
exception types:
@as_result(ValueError, IndexError)
def f(value: int) -> int:
if value == 0:
raise ValueError # becomes Err
elif value == 1:
raise IndexError # becomes Err
elif value == 2:
raise KeyError # raises Exception
else:
return value # becomes Ok
res = f(0) # Err[ValueError()]
res = f(1) # Err[IndexError()]
res = f(2) # raises KeyError
res = f(3) # Ok[3]
Exception
(or even BaseException
) can be specified to create a
‘catch all’ Result
return type. This is effectively the same as try
followed by except Exception
, which is not considered good practice in
most scenarios, and hence this requires explicit opt-in.
Since as_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 @
):
import third_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(...)
if is_ok(res):
print(res.ok_value)
Do notation is syntactic sugar for a sequence of and_then()
calls.
Much like the equivalent in Rust or Haskell, but with different syntax.
Instead of x <- Ok(1)
we write for 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)
for x in Ok(1)
for y in Ok(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 the do
function.
This is similar to Rust's m! macro:
use do_notation::m;
let r = m! {
x <- Some(1);
y <- Some(2);
Some(x + y)
};
Note that if your do statement has multiple for`s, you can access an identifier bound in a previous `for. Example:
my_result: Result[int, str] = do(
f(x, y, z)
for x in get_x()
for y in calculate_y_from_x(x)
for z in calculate_z_from_x_y(x, y)
)
You can use do()
with awaited values as follows:
async def process_data(data) -> Result[int, str]:
res1 = await get_result_1(data)
res2 = await get_result_2(data)
return do(
Ok(x + y)
for x in res1
for y in res2
)
However, if you want to await something inside the expression, use
do_async()
:
async def process_data(data) -> Result[int, str]:
return do_async(
Ok(x + y)
for x in await get_result_1(data)
for y in await get_result_2(data)
)
Troubleshooting do()
calls:
TypeError("Got async_generator but expected generator")
Sometimes regular do()
can handle async values, but this error means
you have hit a case where it does not. You should use do_async()
here
instead.
These steps should work on any Unix-based system (Linux, macOS, etc) with Python
and make
installed. On Windows, you will need to refer to the Python
documentation (linked below) and reference the Makefile
for commands to run
from the non-unix shell you're using on Windows.
- Setup and activate a virtual environment. See Python docs for 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 the changelog
- 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 guard functions:
result: Result[int, str]
if is_ok(result):
reveal_type(result) # "result.result.Ok[builtins.int]"
else:
reveal_type(result) # "result.result.Err[builtins.str]"
if result.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 is a bug in MyPy
which can be triggered in some scenarios. Using if isinstance(res, Ok)
instead of if 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