Lightweight, generic result type à la Rust. (Sometimes you just want errors as values.)
Note: There is a full-on package that does this, with all the other Rust niceties of
.unwrap_or()
, etc. But if all you need is the basics, this is a zero-dependency approach in like 8 lines of actual code.
from dataclasses import dataclass
@dataclass
class Ok[T]:
value: T
@dataclass
class Err[E]:
value: E
type Result[T, E] = Ok[T] | Err[E]
# --- Example -----------------------------------------------------------------
def fn(x: bool) -> Result[int, str]:
if x:
return Ok(1)
else:
return Err("woah")
y = fn(True)
match y:
case Ok(value):
print(value + 1)
case Err(err):
print(err.upper())
If you want a few of the quality-of-life features so you con't have to constantly pattern match:
from dataclasses import dataclass
from typing import Literal, Never
class UnwrapError(Exception):
pass
@dataclass
class Ok[T]:
value: T
def unwrap(self) -> T:
return self.value
def unwrap_or(self, value: T) -> T:
return self.value
def ok(self) -> T:
return self.value
def err(self) -> None:
return None
def is_ok(self) -> Literal[True]:
return True
def is_err(self) -> Literal[False]:
return False
@dataclass
class Err[T, E]:
value: E
def unwrap(self) -> Never:
raise UnwrapError(self.value)
def unwrap_or(self, value: T) -> T:
return value
def ok(self) -> None:
return None
def err(self) -> E:
return self.value
def is_ok(self) -> Literal[False]:
return False
def is_err(self) -> Literal[True]:
return True
type Result[T, E] = Ok[T] | Err[T, E]
def fn(x: int) -> Result[int, str]:
if x > 0:
return Ok(x)
else:
return Err("woah")
assert fn(1).unwrap() == 1
try:
_ = fn(-1).unwrap()
raise AssertionError
except UnwrapError:
pass
assert fn(1).unwrap_or(3) == 1
assert fn(-1).unwrap_or(3) == 3
assert fn(1).ok() == 1
assert fn(-1).ok() is None
assert fn(1).err() is None
assert fn(-1).err() == "woah"
assert fn(1).is_ok() is True
assert fn(-1).is_ok() is False
assert fn(1).is_err() is False
assert fn(-1).is_err() is True
Lightweight, generic result type à la golang.
mypy isn't able to infer without requiring a redundant assert1. I can't figure out how to get a type safe exact go-style implementation, but there's an option below that delays unpacking of the result tuple.
from typing import TypeVar
from typing_extensions import TypeIs
T = TypeVar("T")
E = TypeVar("E")
Ok = tuple[T, None]
Err = tuple[None, E]
Result = Ok[T] | Err[E]
# --- Example -----------------------------------------------------------------
def fn(x: bool) -> Result[int, str]:
if x:
return 1, None
else:
return None, "woah"
# unfortunately mypy can't figure this out:
def main_fail() -> None:
result, err = fn(True)
if err is not None:
print(err.upper())
return
print(result + 1) # error: Operator "+" is not supported for "None"
# --- Example (with assert) ---------------------------------------------------
# so we have to assert, which kind of defeats the point...
def main_pass_with_assert() -> None:
result, err = fn(True)
if err is not None:
print(err.upper())
return
assert result is not None
print(result + 1)
# --- Example (with TypeIs) ---------------------------------------------------
# we could delay unpacking and use a TypeIs function, but then we have to unpack
# the tuple in both branches...
#
# (I think it's up to personal preference if you prefer this or the assert approach)
def is_err(result: Result[T, E]) -> TypeIs[Err[E]]:
_, err = result
return err is not None
def main_pass_with_typeis() -> None:
result = fn(True) # can't unpack or we lose result <-> err type relationship
if is_err(result):
_, err = result
print(err.upper())
return
value, _ = result
print(value + 1)