Skip to content

unwrappy

Rust-inspired Result and Option types for Python

Safe, expressive error handling with errors as values.


  • Explicit Error Handling


    No hidden exceptions. Errors are values that must be handled, making your code's error paths visible and type-checkable.

  • Type Safe


    Full generic type support with proper inference. Works with pyright, mypy, and ty out of the box.

  • Functional Composition


    Rich combinator API (map, and_then, or_else, etc.) for elegant transformation chains.

  • Async First


    LazyResult and LazyOption for clean async operation chaining without nested awaits.

Quick Example

from unwrappy import Ok, Err, Result

def divide(a: int, b: int) -> Result[float, str]:
    if b == 0:
        return Err("division by zero")
    return Ok(a / b)

# Pattern matching (Python 3.10+)
match divide(10, 2):
    case Ok(value):
        print(f"Result: {value}")
    case Err(error):
        print(f"Error: {error}")

# Combinator chaining
result = (
    divide(10, 2)
    .map(lambda x: x * 2)
    .and_then(lambda x: Ok(int(x)) if x < 100 else Err("too large"))
)

Installation

pip install unwrappy

Or with uv:

uv add unwrappy

Why unwrappy?

Python's exception-based error handling has fundamental issues for complex applications:

Problem With Exceptions With unwrappy
Hidden control flow Exceptions can be raised anywhere and caught anywhere Errors are explicit values in function signatures
Unclear contracts Function signatures don't declare what exceptions they raise Return types show exactly what can fail
Silent failures Forgotten exception handling leads to runtime crashes Type checker enforces handling

unwrappy provides Result[T, E] and Option[T] types that make errors explicit values in your code.

Core Types

Result[T, E]

Represents either success (Ok) or failure (Err):

from unwrappy import Ok, Err, Result

def parse_int(s: str) -> Result[int, str]:
    try:
        return Ok(int(s))
    except ValueError:
        return Err(f"invalid number: {s}")

result = parse_int("42")
result.unwrap()      # 42
result.is_ok()       # True

Learn more about Result

Option[T]

Represents an optional value (Some or Nothing):

from unwrappy import Some, NOTHING, Option, from_nullable

def find_user(user_id: int) -> Option[User]:
    user = db.get(user_id)  # Returns User | None
    return from_nullable(user)

email = find_user(123).map(lambda u: u.email).unwrap_or("unknown")

Learn more about Option

LazyResult & LazyOption

Deferred execution for clean async chaining:

from unwrappy import LazyResult

async def fetch_user(id: int) -> Result[User, str]: ...
async def fetch_profile(user: User) -> Result[Profile, str]: ...

# Build pipeline, execute once - no nested awaits!
result = await (
    LazyResult.from_awaitable(fetch_user(42))
    .and_then(fetch_profile)
    .map(lambda p: p.name)
    .collect()
)

Learn more about Lazy Evaluation

Design Philosophy

unwrappy is designed for practical use in real Python codebases:

  • Zero dependencies - Just Python stdlib
  • Incremental adoption - Use in a single module or throughout your codebase
  • Framework friendly - Works alongside FastAPI, Django, Flask, etc.
  • No magic - No decorators, no metaclasses, no import hooks

Read the Architecture docs

Next Steps

  • Getting Started


    Installation, basic usage, and your first Result-based function.

    Get started

  • Guide


    Deep dive into Result, Option, and async patterns.

    Read the guide

  • API Reference


    Complete API documentation with all methods and functions.

    API docs

  • Examples


    Real-world patterns and code examples.

    View examples