Sometimes you just want errors as values.
(With type safety)
Lightweight, generic result type à la Rust.
I think this is way nicer, given the limitations of the Go implementation below.
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 11 lines of actual code.
from dataclasses import dataclass
from typing import TypeVar
T = TypeVar("T")
E = TypeVar("E")
@dataclass
class Ok(Generic[T]):
value: T
@dataclass
class Err(Generic[E]):
value: E
Result = 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())
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)