Skip to main content

Yet another Rust-inspired Result and Option ergonomics brought to Python, enabling safe, expressive error handling with errors as values.

Project description

unwrappy

Rust-inspired Result and Option types for Python, enabling safe, expressive error handling with errors as values.

Installation

pip install unwrappy

Quick Start

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"))
)

Why unwrappy?

  • Explicit error handling: No hidden exceptions, errors are values
  • Type-safe: Full generic type support with proper inference
  • Functional: Rich combinator API (map, and_then, or_else, etc.)
  • Async-first: LazyResult for clean async operation chaining
  • Pattern matching: Works with Python 3.10+ structural matching

Core Types

Result[T, E]

A type that represents either success (Ok) or failure (Err).

from unwrappy import Ok, Err, Result

# Success
ok: Result[int, str] = Ok(42)
ok.unwrap()      # 42
ok.is_ok()       # True

# Error
err: Result[int, str] = Err("failed")
err.unwrap_err() # "failed"
err.is_err()     # True

LazyResult[T, E]

For async operation chaining without nested awaits:

from unwrappy import LazyResult, Ok, Err

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

# Clean async chaining - no nested awaits!
result = await (
    LazyResult.from_awaitable(fetch_user(42))
    .and_then(fetch_profile)
    .map(lambda p: p["name"])
    .map(str.upper)
    .collect()
)

Option[T]

A type that represents an optional value: either Some(value) or Nothing.

from unwrappy import Some, NOTHING, Option, from_nullable

# Has value
some: Option[int] = Some(42)
some.unwrap()      # 42
some.is_some()     # True

# No value
nothing: Option[int] = NOTHING
nothing.is_nothing()  # True

# From nullable Python value
value: str | None = get_optional_value()
opt = from_nullable(value)  # Some(value) or NOTHING

LazyOption[T]

For async operation chaining on optional values:

from unwrappy import LazyOption, Some

async def fetch_config(key: str) -> Option[str]: ...
async def parse_value(s: str) -> Option[int]: ...

# Clean async chaining
result = await (
    LazyOption.from_awaitable(fetch_config("timeout"))
    .and_then(parse_value)
    .map(lambda x: x * 1000)
    .collect()
)

API Overview

Result API

Transformation

Method Description
map(fn) Transform Ok value
map_err(fn) Transform Err value
and_then(fn) Chain Result-returning function
or_else(fn) Recover from Err

Extraction

Method Description
unwrap() Get value or raise UnwrapError
unwrap_or(default) Get value or default
unwrap_or_else(fn) Get value or compute default
unwrap_or_raise(fn) Get value or raise custom exception from fn(error)
expect(msg) Get value or raise with message

Inspection

Method Description
is_ok() / is_err() Check variant
ok() / err() Convert to Option
tee(fn) / inspect(fn) Side effect on Ok
inspect_err(fn) Side effect on Err

Utilities

Function/Method Description
flatten() Unwrap nested Result
split() Convert to (value, error) tuple
filter(predicate, error) Keep Ok if predicate passes
zip(other) / zip_with(other, fn) Combine two Results
context(error) Add context to errors
sequence_results(results) Collect Results into Result
traverse_results(items, fn) Map and collect

Option API

Transformation

Method Description
map(fn) Transform Some value
map_or(default, fn) Transform or return default
map_or_else(default_fn, fn) Transform or compute default
and_then(fn) Chain Option-returning function
or_else(fn) Recover from Nothing
filter(predicate) Keep value if predicate passes

Extraction

Method Description
unwrap() Get value or raise UnwrapError
unwrap_or(default) Get value or default
unwrap_or_else(fn) Get value or compute default
unwrap_or_raise(exc) Get value or raise exception
expect(msg) Get value or raise with message

Inspection

Method Description
is_some() / is_nothing() Check variant
tee(fn) / inspect(fn) Side effect on Some
inspect_nothing(fn) Side effect on Nothing

Utilities

Function/Method Description
from_nullable(value) Convert None to Nothing
flatten() Unwrap nested Option
zip(other) / zip_with(other, fn) Combine two Options
xor(other) Exactly one must be Some
ok_or(err) / ok_or_else(fn) Convert to Result
to_tuple() Convert to single-element tuple
sequence_options(options) Collect Options into Option
traverse_options(items, fn) Map and collect

Examples

Error Recovery

def get_config(key: str) -> Result[str, str]:
    return Err(f"missing: {key}")

# Recover with default
value = get_config("port").unwrap_or("8080")

# Recover with computation
value = (
    get_config("port")
    .or_else(lambda e: Ok("8080"))
    .unwrap()
)

Chaining Operations

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

def validate_positive(n: int) -> Result[int, str]:
    return Ok(n) if n > 0 else Err("must be positive")

result = (
    parse_int("42")
    .and_then(validate_positive)
    .map(lambda x: x * 2)
)
# Ok(84)

Async Operations with LazyResult

async def fetch_user(id: int) -> Result[User, str]:
    # async database call
    ...

async def fetch_posts(user: User) -> Result[list[Post], str]:
    # async API call
    ...

# Build pipeline, execute once
result = await (
    LazyResult.from_awaitable(fetch_user(42))
    .and_then(fetch_posts)              # async
    .map(lambda posts: len(posts))      # sync
    .tee(lambda n: print(f"Found {n}")) # side effect
    .collect()
)

Working with Optional Values

from unwrappy import Some, NOTHING, Option, from_nullable

# Convert nullable Python values
def get_user_email(user_id: int) -> str | None:
    # May return None if user has no email
    ...

email_opt = from_nullable(get_user_email(42))

# Chain operations on optional values
display_name = (
    email_opt
    .map(lambda e: e.split("@")[0])
    .map(str.title)
    .unwrap_or("Anonymous")
)

# Filter with predicates
valid_port = (
    Some(8080)
    .filter(lambda p: 1 <= p <= 65535)
    .unwrap_or(3000)
)

# Convert to Result for error context
result = (
    from_nullable(get_user_email(42))
    .ok_or("User has no email configured")
)

Batch Processing

from unwrappy import Ok, sequence_results, traverse_results

# Collect multiple Results
results = [Ok(1), Ok(2), Ok(3)]
combined = sequence_results(results)  # Ok([1, 2, 3])

# Map and collect
items = ["1", "2", "3"]
parsed = traverse_results(items, parse_int)  # Ok([1, 2, 3])
from unwrappy import Some, NOTHING, sequence_options, traverse_options, from_nullable

# Collect multiple Options
options = [Some(1), Some(2), Some(3)]
combined = sequence_options(options)  # Some([1, 2, 3])

# Fails fast if any is Nothing
options_with_nothing = [Some(1), NOTHING, Some(3)]
combined = sequence_options(options_with_nothing)  # NOTHING

# Map nullable values and collect
items: list[int | None] = [1, 2, 3]
result = traverse_options(items, from_nullable)  # Some([1, 2, 3])

Serialization

unwrappy supports JSON serialization for integration with task queues and workflow frameworks (Celery, Temporal, DBOS, etc.).

from unwrappy import Ok, Err, Some, NOTHING, dumps, loads

# Serialize Result
encoded = dumps(Ok({"key": "value"}))
# '{"__unwrappy_type__": "Ok", "value": {"key": "value"}}'

# Serialize Option
encoded = dumps(Some(42))
# '{"__unwrappy_type__": "Some", "value": 42}'

encoded = dumps(NOTHING)
# '{"__unwrappy_type__": "Nothing"}'

# Deserialize
decoded = loads(encoded)  # Some(42), NOTHING, Ok(...), or Err(...)

For standard json module usage:

import json
from unwrappy import ResultEncoder, result_decoder

encoded = json.dumps(Ok(42), cls=ResultEncoder)
decoded = json.loads(encoded, object_hook=result_decoder)

Note: LazyResult and LazyOption cannot be serialized directly. Call .collect() first to get a concrete Result or Option.

See ARCHITECTURE.md for framework integration examples.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

unwrappy-0.1.0.tar.gz (13.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

unwrappy-0.1.0-py3-none-any.whl (16.5 kB view details)

Uploaded Python 3

File details

Details for the file unwrappy-0.1.0.tar.gz.

File metadata

  • Download URL: unwrappy-0.1.0.tar.gz
  • Upload date:
  • Size: 13.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for unwrappy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 615a697ef9d514d85658f9d8461186fdcc1514b347eee7401d5ce2db521ed1cb
MD5 0a86c62bb443c81c2ef37e7b584f9448
BLAKE2b-256 97ec6f63ec25de0c742ad7c843df23390284a6f183097b42aaf17b0f763a7df7

See more details on using hashes here.

File details

Details for the file unwrappy-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: unwrappy-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 16.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for unwrappy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4fa3c50593a5d105aa4d2046dddb96f9c97abb3b848ad208e6fa5dbde17e578f
MD5 da89e170ca6ec77823faf454ee67faa1
BLAKE2b-256 608ec45a7a66da9a882e3347b2b1b23599488c4b32dc670e9a397c0b268ce1dd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page