rezult
Rust-inspired Result and Option types for Python 3.12+.
rezult makes failure and absence part of the type signature. Instead of
throwing exceptions and hoping every caller remembers to catch them, a
function returns Ok(value) or Err(error), and callers handle both cases
explicitly with pattern matching or the built-in combinators.
rezult is independent, which means it have no extra dependencies. As a result, it is lightweight and fast.
Why not try - except?
Exceptions are invisible in a function signature. Nothing stops an error from escaping several layers up the stack, and the caller has no way to know which failures are expected without reading the implementation.
def parse_age(text: str) -> int:
try:
age = int(text)
except ValueError as exc:
raise RuntimeError("invalid age") from exc
if age < 0:
raise ValueError("age must be non-negative")
return age
try:
age = parse_age("abc")
except RuntimeError as exc:
print(f"failed: {exc}")
With rezult, the failure is part of the return type and the caller handles it
in one place:
from rezult import Err, Ok, resulty
@resulty(ValueError)
def parse_age(text: str) -> int:
age = int(text)
if age < 0:
raise ValueError("age must be non-negative")
return age
match parse_age("abc"):
case Ok(age):
print(f"age = {age}")
case Err(error):
print(f"failed: {error}")
The main differences:
try / except |
rezult |
|
|---|---|---|
| Failure visible in the signature | No | Yes |
| Handling failures | Optional, easy to forget | Explicit match or unwrap |
| Chaining fallible steps | Nested try blocks |
map, map_err, and_then_res, or_else_res |
| Distinguishing "no value" from "error" | None and exceptions get mixed up |
Separate Option and Result types |
| Testing | Must assert on exceptions | Compare values directly |
Notice that rezult is designed for providing a pratical way to handle exceptions more tersely, but not taking the place for exceptions such as KeyboardInterrupted which contain a specified sematic meaning.
Installation
# pip
pip install rezult
# uv
uv add rezult
# uv pip
uv pip install rezult
or, from a checkout:
pip install .
Requires Python 3.12 or newer.
Quick start
Result
from rezult import Err, Ok, Result
def divide(a: float, b: float) -> Result[float, str]:
if b == 0:
return Err("division by zero")
return Ok(a / b)
match divide(10, 2):
case Ok(value):
print(f"result: {value}") # result: 5.0
case Err(error):
print(f"error: {error}")
match divide(1, 0):
case Ok(value):
print(f"result: {value}")
case Err(error):
print(f"error: {error}") # error: division by zero
Results compose without nested try blocks:
def validate(age: int) -> Result[int, str]:
return Ok(age) if age >= 18 else Err("too young")
result = (
divide(36, 2)
.map(lambda value: int(value))
.and_then_res(validate)
)
print(result) # Ok(18)
print(divide(1, 0).map_err(str).unwrap_or(-1)) # -1
Other useful methods include is_ok(), is_err(), ok(), err(),
map_or(), inspect(), unwrap(), expect(), or_res(), and
or_else_res().
Option
Option[T] is either Some(value) or Nothing(). Nothing() is a
singleton, so it can be checked with is.
from rezult import Nothing, Option, Some
def find_user(user_id: int) -> Option[str]:
users = {1: "alice", 2: "bob"}
return Some(users[user_id]) if user_id in users else Nothing()
match find_user(1):
case Some(name):
print(f"hello, {name}") # hello, alice
case Nothing():
print("user not found")
print(find_user(3).unwrap_or("anonymous")) # anonymous
Options chain in the same way:
find_user(1).map(str.upper) # Some('ALICE')
find_user(1).filter(lambda name: name != "alice") # Nothing
find_user(3).or_opt(Some("guest")) # Some('guest')
Other useful methods include is_some(), is_none(), ok_or(),
map_or(), map_or_else(), and_then_opt(), or_else_opt(), xor(),
zip(), and unwrap().
Decorators
resulty turns raised exceptions into Err values:
from pathlib import Path
from rezult import resulty
@resulty()
def load(path: str) -> str:
return Path(path).read_text()
@resulty(ValueError, KeyError)
def lookup(data: dict[str, int], key: str) -> int:
return data[key]
load("missing.txt") # Err(FileNotFoundError(...))
lookup({"a": 1}, "b") # Err(KeyError('b'))
optionally turns a None return value into Nothing():
from rezult import optionally
@optionally
def first(items: list[int]) -> int | None:
return items[0] if items else None
first([1, 2]) # Some(1)
first([]) # Nothing
Both decorators preserve the wrapped function's metadata. They raise
TypeError for async functions instead of silently returning an un-awaited
coroutine.
Evaluating expressions
If you already have a zero-argument callable, use result_eval or
option_eval:
from rezult import option_eval, result_eval
result_eval(lambda: int("42")) # Ok(42)
result_eval(lambda: 1 / 0) # Err(ZeroDivisionError('division by zero'))
option_eval(lambda: None) # Nothing
option_eval(lambda: "value") # Some('value')
API at a glance
| Export | Description |
|---|---|
Ok(value) |
Successful Result variant |
Err(error) |
Failed Result variant |
Some(value) |
Non-empty Option variant |
Nothing() |
Empty Option variant (singleton) |
Result |
Base class and combinators for fallible values |
Option |
Base class and combinators for optional values |
UnwrapError |
Raised by unwrap, expect, unwrap_err, and expect_err |
resulty(...) |
Decorator: exceptions become Err |
optionally |
Decorator: None becomes Nothing() |
result_eval(thunk) |
Evaluate a callable, capturing exceptions as Err |
option_eval(thunk) |
Evaluate a callable, converting None to Nothing() |
Both types support structural pattern matching, equality by variant and contained value, and hashing.
Contributing
We allow all kinds of contributions, except for fully AI generated code and contributions that only contain a little human thinking.
License
MIT
Metadata
Release files for rezult 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| rezult-0.2.0.tar.gz | 9.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rezult-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 20.8 kB
Release files / rezult-0.2.0.tar.gz
| Download URL | rezult-0.2.0.tar.gz |
|---|---|
| Size | 9.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5222f693c9432c9487c0bf3c3299a50baca4b181b224907662e460ced3cbba0c
|
|
BLAKE2b-256 checksum How to use checksums |
a9aa8f8cc7d7af090c53a3d41d0b353e6e9e1462c5bb061ebb84f4e47402887f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / rezult-0.2.0-py3-none-any.whl
| Download URL | rezult-0.2.0-py3-none-any.whl |
|---|---|
| Size | 11.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
411d0f62030cad5d8adb28f77e58d506d9458d6b8e5450c28d4ed13f2bd1d978
|
|
BLAKE2b-256 checksum How to use checksums |
0cf5439cd00128adaf8e96ddd21c66677f2cab79157e0404bacf96179e1d4a77
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for rezult 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.