Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Result

GitHub Workflow Status (branch) Coverage

A simple Result type for Python 3 inspired by Rust, fully type annotated.

Installation

Latest release:

$ pip install result

Latest GitHub master branch version:

$ pip install git+https://github.com/rustedpy/result

Summary

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('ueli@example.com')
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 isinstance(user_result, Ok): # or `is_ok(user_result)`
    # type(user_result.ok_value) == User
    do_something(user_result.ok_value)
else: # or `elif is_err(user_result)`
    # 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. By using isinstance to check for Ok or Err you get type safe access to the contained value when using MyPy to typecheck your code. All of this in a package allowing easier handling of values that can be OK or not, without resorting to custom exceptions.

API

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. You can either use is_ok and is_err type guard functions or isinstance. This way you get type safe access that can be checked with MyPy. The is_ok() or is_err() methods can be used if you don't need the type safety with MyPy:

>>> res = Ok('yay')
>>> isinstance(res, Ok)
True
>>> is_ok(res)
True
>>> isinstance(res, Err)
False
>>> is_err(res)
False
>>> res.is_ok()
True
>>> res.is_err()
False

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.

as_result Decorator

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 isinstance(res, Ok):
    print(res.ok_value)

Do notation

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.

Contributing

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.

  1. Setup and activate a virtual environment. See Python docs for more information about virtual environments and setup.
  2. Run make install to install dependencies
  3. Switch to a new git branch and make your changes
  4. Test your changes:
  • make test
  • make lint
  • You can also start a Python REPL and import result
  1. Update documentation
  • Edit any relevant docstrings, markdown files
  • Run make docs
  1. Add an entry to the changelog
  2. Git commit all your changes and create a new PR.

FAQ

  • 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.

License

MIT License

Release files for result-test-publish-version 0.17.0.dev1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for result-test-publish-version 0.17.0.dev1
File Size Uploaded
result-test-publish-version-0.17.0.dev1.tar.gz 19.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for result-test-publish-version 0.17.0.dev1
File Interpreter ABI Platform
result_test_publish_version-0.17.0.dev1-py3-none-any.whl Python 3 none any Details

Total release size: 30.7 kB

Release files / result-test-publish-version-0.17.0.dev1.tar.gz

Download URL result-test-publish-version-0.17.0.dev1.tar.gz
Size 19.1 kB
Tags Source
SHA-256 checksum
How to use checksums
02cce08291832af58b78db88b66067fdee917ad828efe62f1cdf1376f150f358
BLAKE2b-256 checksum
How to use checksums
01d97c83a9922717263dfc11af60d6a4cc00a196ffc7475c80ab56a921ecd452
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.2 CPython/3.11.6

Release files / result_test_publish_version-0.17.0.dev1-py3-none-any.whl

Download URL result_test_publish_version-0.17.0.dev1-py3-none-any.whl
Size 11.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2bfd84644c3113a57c3a1603a58a1ba40522dee8f90746097461d3015e9a3639
BLAKE2b-256 checksum
How to use checksums
1bc4bcda2646130d4fe42440ec3d127717ea860f92359a2f005394aa0e5e71d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.2 CPython/3.11.6

Release history Release notifications | RSS feed

This release

0.17.0.dev1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page