minmax / result

A simple Rust like Result type for Python 3. Fully type annotated.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Result

Coverage

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

Installation

Latest release:

sh

$ pip install result

Latest GitHub master branch version:

sh

$ 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:

python

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:

python

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.value) == User do_something(user_result.value) else: # or elif is_err(user_result) # type(user_result.value) == str raise RuntimeError('Could not fetch user: %s' % user_result.value)

And if you're using python version 3.10 or later, you can use the elegant match statement as well:

python

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: divide_result = divide(a, b) match divide_result: 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

Creating an instance:

python

>>> 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:

python

>>> 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:

python

>>> 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:

python

>>> res1 = Ok('yay') >>> res2 = Err('nay') >>> res1.ok() 'yay' >>> res2.ok() None

Convert a Result to the error or None:

python

>>> res1 = Ok('yay') >>> res2 = Err('nay') >>> res1.err() None >>> res2.err() 'nay'

Access the value directly, without any other checks:

python

>>> res1 = Ok('yay') >>> res2 = Err('nay') >>> res1.value 'yay' >>> res2.value 'nay'

Note that this is a property, you cannot assign to it. Results are immutable.

For your convenience, simply creating an Ok result without value is the same as using True:

python

>>> res1 = Ok() >>> res1.value True

The unwrap method returns the value if Ok and unwrap_err method returns the error value if Err, otherwise it raises an UnwrapError:

python

>>> res1 = Ok('yay') >>> res2 = Err('nay') >>> res1.unwrap() 'yay' >>> res2.unwrap() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:projectresultresult.py", line 107, in unwrap return self.expect("Called Result.unwrap() on an Err value") File "C:projectresultresult.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:

python

>>> 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:projectresultresult.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:

python

>>> 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:

python

>>> 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:

python

>>> 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:

python

@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 @):

python

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.value)

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

About

A simple Rust like Result type for Python 3. Fully type annotated.

License:MIT License


Languages

Language:Python 100.0%