Skip to main content

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)

Source distribution for rezult 0.2.0
File Size Uploaded Provenance
rezult-0.2.0.tar.gz 9.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for rezult 0.2.0
File Interpreter ABI Platform
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page