0 purchases
safetywrap 1.5.0
safetywrap
Fully typesafe, Rust-inspired wrapper types for Python values
Summary
This library provides two main wrappers: Result and Option. These types
allow you to specify typesafe code that effectively handles errors or
absent data, without resorting to deeply nested if-statements and lots
of try-except blocks.
This is accomplished by allowing you to operate on an Option or Result
in a sort of quantum superposition, where an Option could be Some or
Nothing or a Result could be Ok or Err. In either case, all of the
methods on the type work just the same, allowing you to handle both cases
elegantly.
A Result[T, E] may be an instance of Ok[T] or Err[E], while an Option[T]
may be an instance of Some[T] or Nothing. Either way, you get to treat
them just the same until you really need to get the wrapped value.
So, rather than this:
for something in "value", None:
if something is not None:
val = something.upper()
else:
val = "DEFAULT"
print(val)
You can do this:
for something in Some("value"), Nothing():
print(something.map(str.upper).unwrap_or("DEFAULT"))
And rather than this:
for jsondata in '{"value": "myvalue"}', '{badjson':
try:
config = capitalize_keys(json.loads(jsondata))
except Exception:
config = get_default_config()
print(config["value"])
You can do this:
for jsondata in '{"value": "myvalue"}', '{badjson':
print(
Result.of(json.loads, jsondata)
.map(capitalize_keys)
.unwrap_or_else(get_default_config)["value"]
)
These two examples are super minimal samples of how using these typesafe
wrappers can make things easier to write and reason about. Please see the
Examples section for more, and Usage for the full
suite of offered functionality.
These types are heavily influenced by the Result and
Option types in Rust.
Thorough type specifications for mypy or your favorite python type-checker
are provided, so that you can decorate function inputs and outputs as
returning Result and Option types and get useful feedback when supplying
arguments or passing return values.
Sponsorship
This project was developed for and is graciously sponsored by my employer,
Bestow, Inc.. At Bestow, we aim to democratize life
insurance by providing simple, easy coverage, purchasable online in five minutes
with no doctors' visits and no hassles.
We're pretty much always hiring great developers, so if you'd like to work
with us, please check out our careers page!
Table of Contents
safetywrap
Summary
Sponsorship
Table of Contents
Examples
Get an enum member by its value, returning the member or None
Get an enum member by its value, returning an Option
Serialize a dict that may be missing keys, using default values
Make an HTTP request, and if the status code is 200, convert the body to JSON and return the data key. If there is an error or the data key does not exist, return an error string
Usage
Result[T, E]
Result Constructors
Ok
Err
Result.of
Result.collect
Result.err_if
Result.ok_if
Result Methods
Result.and_
Result.or_
Result.and_then
Result.flatmap
Result.or_else
Result.err
Result.ok
Result.expect
Result.raise_if_err
Result.expect_err
Result.is_err
Result.is_ok
Result.iter
Result.map
Result.map_err
Result.unwrap
Result.unwrap_err
Result.unwrap_or
Result.unwrap_or_else
Result Magic Methods
Option[T]
Option Constructors
Some
Nothing
Option.of
Option.nothing_if
Option.some_if
Option.collect
Option Methods
Option.and_
Option.or_
Option.xor
Option.and_then
Option.flatmap
Option.or_else
Option.expect
Option.raise_if_nothing
Option.filter
Option.is_nothing
Option.is_some
Option.iter
Option.map
Option.map_or
Option.map_or_else
Option.ok_or
Option.ok_or_else
Option.unwrap
Option.unwrap_or
Option.unwrap_or_else
Option Magic Methods
Performance
Results
Discussion
Contributing
Examples
In general, these examples build from simple to complex. See Usage
below for the full API specification.
Get an enum member by its value, returning the member or None
import typing as t
from enum import Enum
from result_types import Option, Result, Some
T = t.TypeVar("T", bound=Enum)
def enum_member_for_val(enum: t.Type[T], value: t.Any) -> t.Optional[t.Any]:
"""Return Some(enum_member) or Nothing()."""
# Enums throw a `ValueError` if the value isn't present, so
# we'll either have `Ok(enum_member)` or `Err(ValueError)`.
# We unwrap and return the member if it's Ok, otherwise, we just
# return None
return Result.of(enum, value).unwrap_or(None)
Get an enum member by its value, returning an Option
import typing as t
from enum import Enum
from result_types import Option, Result, Some
T = t.TypeVar("T", bound=Enum)
def enum_member_for_val(enum: t.Type[T], value: t.Any) -> Option[T]:
"""Return Some(enum_member) or Nothing()."""
# Enums throw a `ValueError` if the value isn't present, so
# we'll either have `Ok(enum_member)` or `Err(ValueError)`.
# Calling `ok()` on a `Result` returns an `Option`
return Result.of(enum, value).ok()
Serialize a dict that may be missing keys, using default values
import json
from result_types import Result
def serialize(data: t.Dict[str, t.Union[int, str, float]]) -> str:
"""Serialize the data.
Absent keys are "[absent]", rather than null. This allows us to maintain
information about whether a key was present or actually set to None.
"""
keys = ("first", "second", "third", "fourth")
# We can even use Result to catch any JSON serialization errors, so that
# this function will _always_ return a string!
Result.of(
json.dumps,
# Result.of() will intercept the KeyError and return an Err. We use
# `unwrap_or()` to discard the error and return the "[absent]" string
# instead; if the key was present, the Result was Ok, and we just
# return that value.
{k: Result.of(lambda: data[k]).unwrap_or("[absent]") for k in keys}
).unwrap_or("Could not serialize JSON from data!")
Make an HTTP request, and if the status code is 200, convert the body to JSON and return the data key. If there is an error or the data key does not exist, return an error string
from functools import partial
import requests
from requests import Response
from result_types import Option, Result
def get_data(url: str) -> str:
"""Get the data!"""
# We need to do manual type assignment sometimes when the code
# we're wrapping does not provide types.
# If the wrapped function raises any Exception, `res` will be
# Err(Exception). Otherwise it will be `Ok(Response)`.
res: Result[Response, Exception] = Result.of(requests.get, url)
return (
# We start as a `Result[Response, Exception]`
res
# And if we were an Err, map to a `Result[Response, str]`
.map_err(str)
# If we were Ok, and_then (aka flatmap) to a new `Result[Response, str]`
.and_then(lambda res: (
# Our return value starts as a `Result[Response, Response]`
Result.ok_if(lambda r: r.status_code == 200, res).map_err(
# So we map it to a `Result[Response, str]`
lambda r: str(f"Bad status code: {r.status_code}")
)
))
# We are now a `Result[Response, str]`, where we are only Ok if
# our status code was 200.
# Now we transition to a `Result[dict, str]`
.and_then(lambda res: Result.of(res.json).map_err(str))
# And to a `Result[Option[str], str]`
.map(lambda js: Option.of(js.get("data")).map(str))
# And to a `Result[str, str]`
.and_then(lambda data: data.ok_or("No data key in JSON!"))
# If we are an error, convert us to an Ok with the error string
.or_else(Ok)
# And now we get either the Ok string or the Err string!
.unwrap()
)
Usage
Result[T, E]
A Result represents some value that may either be in an Ok state or
an Err state.
Result Constructors
Ok
Ok(value: T) -> Result[T, E]
Construct an Ok Result directly with the value.
Example:
def check_value_not_negative(val: int) -> Result[int, str]:
"""Check that a value is not negative, or return an Err."""
if val >= 0:
return Ok(val)
return Err(f"{val} is negative!")
Err
Err(value: E) -> Result[T, E]
Construct an Err Result directly with the value.
Example:
def check_value_not_negative(val: int) -> Result[int, str]:
"""Check that a value is not negative, or return an Err."""
if val >= 0:
return Ok(val)
return Err(f"{val} is negative!")
Result.of
Result.of(fn: Callable[..., T], *args: t.Any, catch: t.Type[E], **kwargs) -> Result[T, E]
Call a function with the provided arguments. If no error is thrown, return
Ok(result). Otherwise, return Err(exception). By default, Exception
is caught, but different error types may be provided with the catch
keyword argument.
The type of E MUST be Exception or one of its subclasses.
This constructor is designed to be useful in wrapping other APIs, builtin
functions, etc.
Note that due to a bug in mypy (see https://github.com/python/mypy/issues/3737),
sometimes you need to explicitly specify the catch keyword argument,
even if you're setting it to its default (Exception). This does not
happen consistently, but when it does, you will see mypy thinking
that the type of the Result is Result[SomeType, <nothing>].
Example:
import json
def parse_json(string: str) -> Result[dict, Exception]:
"""Parse a JSON object into a dict."""
return Result.of(json.loads, string)
Result.collect
Result.collect(iterable: Iterable[T, E]) -> Result[Tuple[T, ...], E]
Convert an iterable of Results into a single Result. If all Results were
Ok, Ok values are collected into a Tuple in the final Result. If any Results
were Err, the Err result is returned directly.
Example:
assert Result.collect([Ok(1), Ok(2), Ok(3)]) == Ok((1, 2, 3))
assert Result.collect([Ok(1), Err("no"), Ok(3)]) == Err("no")
Result.err_if
Result.err_if(predicate: t.Callable[[T], bool], value: T) -> Result[T, T]
Run a predicate on some value, and return Err(val) if the predicate returns
True, or Ok(val) if the predicate returns False.
Example:
from requests import Response
def checked_response(response: Response) -> Result[Response, Response]:
"""Try to get a response from the server."""
return Result.err_if(lambda r: r.status_code >= 300, response)
Result.ok_if
Result.ok_if(predicate: t.Callable[[T], bool], value: T) -> Result[T, T]
Run a predicate on some value, and return Ok(val) if the predicate returns
True, or Err(val) if the predicate returns False.
Example:
def checked_data(data: dict) -> Result[dict, dict]:
"""Check if data has expected keys."""
expected_keys = ("one", "two", "three")
return Result.ok_if(lambda d: all(k in d for k in expected_keys), data)
Result Methods
Result.and_
Result.and_(self, res: Result[U, E]) -> Result[U, E]
If this Result is Ok, return res. If this result is Err, return this
Result. This can be used to short circuit a chain of Results on encountering
the first error.
Example:
assert Ok(5).and_(Ok(6)) == Ok(6)
assert Err(1).and_(Ok(6)) == Err(1)
assert Err(1).and_(Err(2)).and_(Ok(5)) == Err(1)
assert Ok(5).and_(Err(1)).and_(Ok(6)) == Err(1)
Result.or_
Result.or_(self, res: Result[T, F]) -> Result[T, F]
If this Result is Err, return res. Otherwise, return this Result.
Example:
assert Err(1).or_(Ok(5)) == Ok(5)
assert Err(1).or_(Err(2)) == Err(2)
assert Ok(5).or_(Ok(6)) == Ok(5)
assert Ok(5).or_(Err(1)) == Ok(5)
Result.and_then
Result.and_then(self, fn: t.Callable[[T], Result[U, E]]) -> Result[U, E]
If this Result is Ok, call the provided function with the wrapped value of
this Result and return the Result of that function. This allows easily
chaining multiple Result-generating calls together to yield a final
Result. This method is an alias of Result.flatmap
Example:
assert Ok(5).and_then(lambda val: Ok(val + 1)) == Ok(6)
assert Err(1).and_then(lambda val: Ok(val + 1)) == Err(1)
Result.flatmap
Result.flatmap(self, fn: t.Callable[[T], Result[U, E]]) -> Result[U, E]
If this Result is Ok, call the provided function with the wrapped value of
this Result and return the Result of that function. This allows easily
chaining multiple Result-generating calls together to yield a final
Result. This method is an alias of Result.and_then
Example:
assert Ok(5).flatmap(lambda val: Ok(val + 1)) == Ok(6)
assert Err(1).flatmap(lambda val: Ok(val + 1)) == Err(1)
Result.or_else
Result.or_else(self, fn: t.Callable[[E], Result[T, F]]) -> Result[T, F])
If this result is Err, call the provided function with the wrapped error
value of this Result and return the Result of that function. This allows
easily handling potential errors in a way that still returns a final Result.
Example:
assert Ok(5).or_else(Ok) == Ok(5)
assert Err(1).or_else(Ok) == Ok(1)
Result.err
Result.err(self) -> Option[E]
Convert this Result into an Option, returning Some(err_val) if this Result
is Err, or Nothing() if this Result is Ok.
Example:
assert Ok(5).err() == Nothing()
assert Err(1).err() == Some(1)
Result.ok
Result.ok(self) -> Option[T]
Convert this Result into an Option, returning Some(val) if this Result is
Ok, or Nothing() if this result is Err.
Example:
assert Ok(5).ok() == Some(5)
assert Err(1).ok() == Nothing()
Result.expect
Result.expect(self, msg: str, exc_cls: t.Type[Exception] = RuntimeError) -> T
Return the wrapped value if this Result is Ok. Otherwise, raise an error,
instantiated with the provided message and the stringified error value.
By default, a RuntimeError is raised, but an alternative error may be
provided using the exc_cls keyword argument. This method is an alias for
Result.raise_if_err.
Example:
import pytest
with pytest.raises(RuntimeError) as exc:
Err(5).expect("Bad value")
assert str(exc.value) == "Bad value: 5"
assert Ok(1).expect("Bad value") == 1
Result.raise_if_err
Result.raise_if_err(self, msg: str, exc_cls: t.Type[Exception] = RuntimeError) -> T
Return the wrapped value if this Result is Ok. Otherwise, raise an error,
instantiated with the provided message and the stringified error value.
By default, a RuntimeError is raised, but an alternative error may be
provided using the exc_cls keyword argument. This method is an alias for
Result.expect.
Example:
import pytest
with pytest.raises(RuntimeError) as exc:
Err(5).raise_if_err("Bad value")
assert str(exc.value) == "Bad value: 5"
assert Ok(1).raise_if_err("Bad value") == 1
Result.expect_err
Result.expect_err(self, msg: str, exc_cls: t.Type[Exception] = RuntimeError) -> E
Return the wrapped value if this Result is Err. Otherwise, raise an error,
instantiated with the provided message and the stringified Ok value.
By default, a RuntimeError is raised, but an alternative error may be
provided using the exc_cls keyword argument.
Example:
import pytest
with pytest.raises(RuntimeError) as exc:
Ok(5).expect_err("Unexpected good value")
assert str(exc.value) == "Unexpected good value: 5"
assert Err(1).expect_err("Unexpected good value") == 1
Result.is_err
Result.is_err(self) -> bool
Return True if this Result is Err, or False if this Result is Ok.
Example:
assert Err(1).is_err() is True
assert Ok(1).is_err() is False
Result.is_ok
Result.is_ok(self) -> bool
Return True if this Result is Ok, or False if this Result is Err.
Example:
assert Ok(1).is_err() is True
assert Err(1).is_err() is False
Result.iter
Result.iter(self) -> Iterator[T]
Return an iterator with length 1 over the wrapped value if this Result is Ok.
Otherwise, return a 0-length iterator.
Example:
assert tuple(Ok(1).iter()) == (1,)
assert tuple(Err(1).iter()) == ()
Result.map
Result.map(self, fn: t.Callable[[T], U]) -> Result[U, E]
If this Result is Ok, apply the provided function to the wrapped value,
and return a new Ok Result with the result of the function. If this Result
is Err, do not apply the function and return this Result unchanged.
Warning: no error checking is performed while applying the provided
function, and exceptions applying the function are not caught. If you need
to map with error handling, consider using and_then (aka flatmap) in
conjunction with the Result.of constructor, e.g.
assert Ok(0).and_then(partial(Result.of, lambda i: 10 / i)) == Err(ZeroDivisionError('division by zero'))
Example:
assert Ok(1).map(str) == Ok("1")
assert Err(1).map(str) == Err(1)
Result.map_err
Result.map_err(self, fn: t.Callable[[E], F]) -> Result[T, F]
If this Result is Err, apply the provided function to the wrapped value,
and return a new Err Result with the result of the function. If this Result
is Ok, do not apply the function and return this Result unchanged.
Warning: no error checking is performed while applying the provided
function, and exceptions applying the function are not caught.
Example:
assert Err(1).map_err(lambda i: i + 1) == Err(2)
assert Ok(1).map_err(lambda i: i + 1) == Ok(1)
Result.unwrap
Result.unwrap(self) -> T
If this Result is Ok, return the wrapped value. If this Result is Err,
throw a RuntimeError.
Example:
import pytest
assert Ok(1).unwrap() == 1
with pytest.raises(RuntimeError):
Err(1).unwrap()
Result.unwrap_err
Result.unwrap_err(self) -> E
If this Result is Err, return the wrapped value. If this Result is Ok,
throw a RuntimeError.
Example:
import pytest
assert Err(1).unwrap() == 1
with pytest.raises(RuntimeError):
Ok(1).unwrap()
Result.unwrap_or
Result.unwrap_or(self, alternative: U) -> t.Union[T, U]
If this Result is Ok, return the wrapped value. Otherwise, if this Result
is Err, return the provided alternative.
Example:
assert Ok(1).unwrap_or(5) == 1
assert Err(1).unwrap_or(5) == 5
Result.unwrap_or_else
Result.unwrap_or_else(self, fn: t.Callable[[E], U]) -> t.Union[T, U]
If this Result is Ok, return the wrapped value. Otherwise, if this Result
is Err, call the supplied function with the wrapped error value and return
the result.
Example:
assert Ok(1).unwrap_or_else(str) == 1
assert Err(1).unwrap_or_else(str) == "1"
Result Magic Methods
Result.iter
Result.__iter__(self) -> t.Iterator[T]
Implement the iterator protocol, allowing iteration over the results of
Result.iter. If this Result is Ok, return an iterator
of length 1 containing the wrapped value. Otherwise, if this Result is Err,
return a 0-length iterator.
Example:
# Can be passed to methods that take iterators
assert tuple(Ok(1)) == (1,)
assert tuple(Err(1)) == ()
# Can be used in `for in` constructs, including comprehensions
assert [val for val in Ok(5)] == [5]
assert [val for val in Err(5)] == []
# More for-in usage.
for val in Ok(5):
pass
assert val == 5
val = None
for val in Err(1):
pass
assert val is None
Result.eq
Result.__eq__(self, other: Any) -> bool
Enable equality checking using ==.
Compare the Result with other. Return True if other is the same type of
Result with the same wrapped value. Otherwise, return False.
Example:
assert (Ok(5) == Ok(5)) is True
assert (Ok(5) == Ok(6)) is False
assert (Ok(5) == Err(5)) is False
assert (Ok(5) == 5) is False
Result.ne
Result.__ne__(self, other: Any) -> bool
Enable inequality checking using !=.
Compare the Result with other. Return False if other is the same type of
Result with the same wrapped value. Otherwise, return True.
Example:
assert (Ok(5) != Ok(5)) is False
assert (Ok(5) != Ok(6)) is True
assert (Ok(5) != Err(5)) is True
assert (Ok(5) != 5) is True
Result.str
Result.__str__(self) -> str
Enable useful stringification via str().
Example:
assert str(Ok(5)) == "Ok(5)"
assert str(Err(5)) == "Err(5)"
Result.repr
Result.__repr__(self) -> str
Enable useful stringification via repr().
Example:
assert repr(Ok(5)) == "Ok(5)"
assert repr(Err(5)) == "Err(5)"
Option[T]
An Option represents either Some value or Nothing.
Option Constructors
Some
Some(value: T) -> Option[T]
Construct a Some Option directly with a value.
Example:
def file_contents(path: str) -> Option[str]:
"""Return the file contents or Nothing."""
try:
with open(path) as f:
return Some(f.read())
except IOError:
return Nothing()
Nothing
Nothing() -> Option[T]
Construct a Nothing Option directly with a value.
Note: as an implementation detail, Nothing is implemented as a singleton,
to avoid instantiation time for any Nothing created after the first.
However since this is an implementation detail, Nothing Options should
still be compared with == rather than is.
Example:
def file_contents(path: str) -> Option[str]:
"""Return the file contents or Nothing."""
try:
with open(path) as f:
return Some(f.read())
except IOError:
return Nothing()
Option.of
Option.of(value: t.Optional[T]) -> Option[T]
Convert an optional value into an Option. If the value is not None, return
Some(value). Otherwise, if the value is None, return Nothing().
Example:
assert Option.of(None) == Nothing()
assert Option.of({}.get("a")) == Nothing()
assert Option.of("a") == Some("a")
assert Option.of({"a": "b"}) == Some("b")
Option.nothing_if
Option.nothing_if(predicate: t.Callable[[T], bool], value: T) -> Option[T]
Call the provided predicate function with the provided value. If the predicate
returns True, return Nothing(). If the predicate returns False, return
Some(value).
Example:
assert Option.nothing_if(lambda val: val.startswith("_"), "_private") == Nothing()
assert Option.nothing_if(lambda val: val.startswith("_"), "public") == Some("public")
Option.some_if
Option.some_if(predicate: t.Callable[[T], bool], value: T) -> Option[T]
Call the provided predicate function with the provided value. If the predicate
returns True, return Some(value). If the predicate returns False, return
Nothing().
Example:
assert Option.some_if(bool, [1, 2, 3]) == Some([1, 2, 3])
assert Option.some_if(bool, []) == Nothing()
Option.collect
Option.collect(options: t.Iterable[Option[T]]) -> Option[t.Tuple[T, ...]]
Collect a series of Options into single Option.
If all options are Some[T], the result is Some[Tuple[T, ...j]]. If
any options are Nothing, the result is Nothing.
Example:
assert Option.collect([Some(1), Some(2), Some(3)]) == Some((1, 2, 3))
assert Option.collect([Some(1), Nothing(), Some(3)]) == Nothing()
Option Methods
Option.and_
Option.and_(alternative: Option[U]) -> Option[U]
If this Option is Nothing, return it unchanged. Otherwise, if this Option
is Some, return the provided alternative Option.
Example:
assert Some(1).and_(Some(2)) == Some(2)
assert Nothing().and_(Some(2)) == Nothing()
assert Some(1).and_(Nothing()) == Nothing()
assert Nothing().and_(Nothing()) == Nothing()
assert Some(1).and_(Nothing()).and_(Some(2)) == Nothing()
Option.or_
Option.or_(alternative: Option[T]) -> Option[T]
If this Option is Nothing, return the provided alternative Option.
Otherwise, if this Option is Some, return it unchanged.
Example:
assert Some(1).or_(Some(2)) == Some(1)
assert Some(1).or_(Nothing()) == Some(1)
assert Nothing().or_(Some(1)) == Some(1)
assert Nothing().or_(Nothing()) == Nothing()
Option.xor
Option.xor(alternative: Option[T]) -> Option[T]
Exclusive or. Return Some Option iff (if and only if) exactly one of
this Option and hte provided alternative are Some. Otherwise, return
Nothing.
Example:
assert Some(1).xor(Nothing()) == Some(1)
assert Nothing().xor(Some(1)) == Some(1)
assert Some(1).xor(Some(2)) == Nothing()
assert Nothing().xor(Nothing()) == Nothing()
Option.and_then
Option.and_then(self, fn: t.Callable[[T], Option[U]]) -> Option[U]
If this Option is Some, call the provided, Option-returning function with
the contained value and return whatever Option it returns. If this Option
is Nothing, return it unchanged. This method is an alias for
Option.flatmap
Example:
assert Some(1).and_then(lambda i: Some(i + 1)) == Some(2)
assert Nothing().and_then(lambda i: Some(i + 1)) == Nothing()
Option.flatmap
Option.flatmap(self, fn: t.Callable[[T], Option[U]]) -> Option[U]
If this Option is Some, call the provided, Option-returning function with
the contained value and return whatever Option it returns. If this Option
is Nothing, return it unchanged. This method is an alias for
Option.and_then
Example:
assert Some(1).flatmap(Some) == Some(1)
assert Nothing().flatmap(Some) == Nothing()
Option.or_else
Option.or_else(self, fn: t.Callable[[], Option[T]]) -> Option[T]
If this Option is Nothing, call the provided, Option-returning function
and return whatever Option it returns. If this Option is Some, return it
unchanged.
Example:
assert Nothing().or_else(lambda: Some(1)) == Some(1)
assert Some(1).or_else(lambda: Some(2)) == Some(1)
Option.expect
Option.expect(self, msg: str, exc_cls: t.Type[Exception] = RuntimeError) -> T
If this Option is Some, return the wrapped value. Otherwise, if this
Option is Nothing, raise an error instantiated with the provided message.
By default, a RuntimeError is raised, but a custom exception class may be
provided via the exc_cls keyword argument. This method is an alias
of Option.raise_if_nothing.
Example:
import pytest
with pytest.raises(RuntimeError) as exc:
Nothing().expect("Nothing here")
assert str(exc.value) == "Nothing here"
assert Some(1).expect("Nothing here") == 1
Option.raise_if_nothing
Option.raise_if_nothing(self, msg: str, exc_cls: t.Type[Exception] = RuntimeError) -> T
If this Option is Some, return the wrapped value. Otherwise, if this
Option is Nothing, raise an error instantiated with the provided message.
By default, a RuntimeError is raised, but a custom exception class may be
provided via the exc_cls keyword argument. This method is an alias
of Option.expect.
Example:
import pytest
with pytest.raises(RuntimeError) as exc:
Nothing().raise_if_nothing("Nothing here")
assert str(exc.value) == "Nothing here"
assert Some(1).raise_if_nothing("Nothing here") == 1
Option.filter
Option.filter(self, predicate: t.Callable[[T], bool]) -> Option[T]
If this Option is Some, call the provided predicate function with the wrapped
value. If the predicate returns True, return Some containing the wrapped
value of this Option. If the predicate returns False, return Nothing. If
this Option is Nothing, return it unchanged.
Example:
def is_even(val: int) -> bool:
"""Return whether the value is even."""
return val % 2 == 0
assert Some(2).filter(is_even) == Some(2)
assert Some(1).filter(is_even) == Nothing()
assert Nothing().filter(is_even) == Nothing()
Option.is_nothing
Option.is_nothing(self) -> bool
If this Option is Nothing, return True. Otherwise, if this Option is
Some, return False.
Example:
assert Nothing().is_nothing() is True
assert Some(1).is_nothing() is False
Option.is_some
Option.is_some(self) -> bool
If this Option is Some. Otherwise, if this Option is Nothing, return False.
Example:
assert Some(1).is_some() is True
assert Nothing().is_some() is False
Option.iter
Option.iter(self) -> t.Iterator[T]
If this Option is Some, return an iterator of length one over the wrapped
value. Otherwise, if this Option is Nothing, return a 0-length iterator.
Example:
assert tuple(Some(1).iter()) == (1,)
assert tuple(Nothing().iter()) == ()
Option.map
Option.map(self, fn: t.Callable[[T], U]) -> Option[U]
If this Option is Some, apply the provided function to the wrapped value,
and return Some wrapping the result of the function. If this Option is
Nothing, return this Option unchanged.
Example:
assert Some(1).map(str) == Some("1")
assert Nothing().map(str) == Nothing()
assert Some(1).map(str).map(lambda x: x + "a").map(str.upper) == Some("1A")
Option.map_or
Option.map_or(self, default: U, fn: t.Callable[[T], U]) -> U
If this Option is Some, apply the provided function to the wrapped value
and return the result. If this Option is Nothing, return the provided
default value.
Example:
assert Some(1).map_or("no value", str) == "1"
assert Nothing().map_or("no value", str) == "no value"
Option.map_or_else
Option.map_or_else(self, default: t.Callable[[], U], fn: t.Callable[[T], U]) -> U
If this Option is Some, apply the provided function to the wrapped value and
return the result. If this Option is Nothing, call the provided default
function with no arguments and return the result.
Example:
from datetime import datetime, date
assert Some("2005-08-28").map_or_else(
date.today,
lambda t: datetime.strptime(t, "%Y-%m-%d").date()
) == datetime(2005, 8, 28).date()
assert Nothing().map_or_else(
date.today,
lambda t: datetime.strptime(t, "%Y-%m-%d").date()
) == date.today()
Option.ok_or
Option.ok_or(self, err: E) -> Result[T, E]
If this Option is Some, return an Ok Result wrapping the contained
value. Otherwise, return an Err result wrapping the provided error.
Example:
assert Some(1).ok_or("no value!") == Ok(1)
assert Nothing().ok_or("no value!") == Err("no value!")
Option.ok_or_else
Option.ok_or_else(self, err_fn: t.Callable[[], E]) -> Result[T, E]
If this Option is Some, return an Ok Result wrapping the contained
value. Otherwise, call the provided err_fn and wrap its return value
in an Err Result.
Example:
from functools import partial
def make_err_msg(msg: str) -> str:
"""Make an error message with some starting text."""
return f"[MY_APP_ERROR] -- {msg}"
assert Some(1).ok_or_else(partial(make_err_msg, "no value!")) == Ok(1)
assert Nothing().ok_or_else(partial(make_err_msg, "no value!")) == Err(
"[MY_APP_ERROR] -- no value!"
)
Option.unwrap
Option.unwrap(self) -> T
If this Option is Some, return the wrapped value. Otherwise, raise a
RuntimeError.
Example:
import pytest
assert Some(1).unwrap() == 1
with pytest.raises(RuntimeError):
Nothing().unwrap()
Option.unwrap_or
Option.unwrap_or(self, default: U) -> t.Union[T, U]
If this Option is Some, return the wrapped value. Otherwise, return the
provided default.
Example:
assert Some(1),unwrap_or(-1) == 1
assert Nothing().unwrap_or(-1) == -1
Option.unwrap_or_else
Option.unwrap_or_else(self, fn: t.Callable[[], U]) -> t.Union[T, U]
If this Option is Some, return the wrapped value. Otherwise, return the
result of the provided function.
Example:
from datetime import date
assert Some(date(2001, 1, 1)).unwrap_or_else(date.today) == date(2001, 1, 1)
assert Nothing().unwrap_or_else(date.today) == date.today()
Option Magic Methods
Option.iter
Option.__iter__(self) -> t.Iterator[T]
Implement the iterator protocol, allowing iteration over the results of
Option.iter. If this Option is Ok, return an iterator
of length 1 containing the wrapped value. Otherwise, if this Option is Nothing,
return a 0-length iterator.
Example:
# Can be passed to methods that take iterators
assert tuple(Some(1)) == (1,)
assert tuple(Nothing()j) == ()
# Can be used in `for in` constructs, including comprehensions
assert [val for val in Some(1)] == [1]
assert [val for val in Nothing()] == []
# More for-in usage.
for val in Some(1):
pass
assert val == 1
val = None
for val in Nothing():
pass
assert val is None
Option.eq
Option.__eq__(self, other: Any) -> bool
Enable equality checking using ==.
Compare this Option with other. Return True if other is the same type of
Option with the same wrapped value. Otherwise, return False.
Example:
assert (Some(1) == Some(1)) is True
assert (Some(1) == Some(2)) is False
assert (Some(1) == Nothing()) is False
assert (Some(1) == 1) is False
Option.ne
Option.__ne__(self, other: Any) -> bool
Enable inequality checking using !=.
Compare the Option with other. Return False if other is the same type of
Option with the same wrapped value. Otherwise, return True.
Example:
assert (Some(1) != Some(1)) is False
assert (Some(1) != Some(2)) is True
assert (Some(1) != Nothing()) is True
assert (Some(1) != 1) is True
Option.str
Option.__str__(self) -> str
Enable useful stringification via str().
Example:
assert str(Some(1)) == "Some(1)"
assert str(Nothing()) == "Nothing()"
Option.repr
Option.__repr__(self) -> str
Enable useful stringification via repr().
Example:
assert repr(Some(1)) == "Some(1)"
assert repr(Nothing()) == "Nothing()"
Performance
Benchmarks may be run with make bench. Benchmarking utilities are provided
in bench/.
Currently, the sample.py benchmark defines two data
stores, one using classical python error handling (or lack thereof), and
the other using this library's wrapper types. Some simple operations
are performed using each data store for comparison.
runner.sh runs the benchmarks two ways. First, it uses
hyperfine to run the benchmarks as a normal python script 100 times and
display information about the run time. It then uses python's builtin
timeit module to measure
the code execution time in isolation over one million runs, without the
added overhead of spinning up the interpreter to parse and run the script.
Results
The Result and Option wrapper types add minimal overhead to
execution time, which will not be noticeable for most real-world workloads.
However, care should be taken if using these types in "hot paths."
Run in isolation, the sample code using Result and Option types is
about six times slower than builtin exception handling:
Method
Number of Executions
Average Execution Time
Relative to Classical
Classical
1,000,000 (1E6)
3.79E-6 s (3.79 μs)
1x
Wrapper
1,000,000 (1E6)
2.31E-5 s (23.1 μs)
6.09x
When run as part of a Python script, there is no significant difference
between using code with these wrapper types versus code that uses builtin
exception handling and nested if statements.
Method
Number of Executions
Average Execution Time
Relative to Classical
Classical
100
32.2 ms
1x
Wrapper
100
32.5 ms
1.01x
Discussion
Care has been taken to make the wrapper types in this library as performant
as possible. All types use __slots__ to avoid allocating a dictionary for
instance variables, and wrapper variants (e.g. Ok and Err for Result)
are implemented as separate subclasses of Result rather than a shared
class in order to avoid needing to perform if/else branching or isinstance()
checks, which are notoriously slow in Python.
That being said, using these types is doing more than the builtin error
handling! Instances are being constructed and methods are being accessed.
Both of these are relatively quick in Python, but definitely not quicker
than doing nothing, so this library will probably never be quite as performant
as raw exception handling. That being said, that is not its aim! The goal
is to be as quick as possible, preferably within striking distance of
regular old idiomatic python, while providing significantly more ergonomics
and type safety around handling errors and absent data.
Contributing
Contributions are welcome! To get started, you'll just need a local install
of Python 3.
Once you've forked and cloned the repo, you can run:
make test - run tests using your local interpreter
make fmt - format code using black
make lint - check code with a variety of analysis tools
make bench - run benchmarks
See the Makefile for other commands.
The CI system requires that make lint and make test run successfully
(exit status of 0) in order to merge code.
result_types is compatible with Python >= 3.6. You can run against
all supported python versions with make test-all-versions. This requires
that docker be installed on your local system. Alternatively, if you
have all required Python versions installed, you may run make tox to
run against your local interpreters.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.