{"id":38700433,"url":"https://github.com/leodiegues/unwrappy","last_synced_at":"2026-01-17T10:47:33.290Z","repository":{"id":332439443,"uuid":"1133452375","full_name":"leodiegues/unwrappy","owner":"leodiegues","description":"Yet another Rust-inspired Result and Option ergonomics brought to Python, enabling safe, expressive error handling with errors as values.","archived":false,"fork":false,"pushed_at":"2026-01-13T21:19:20.000Z","size":99,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-13T22:31:05.983Z","etag":null,"topics":["async","either-type","error-handling","functional-programming","maybe-type","monads","option-type","pattern-matching","python","railway-oriented-programming","result-type","rust","rust-inspired","type-hints","type-safe"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leodiegues.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-13T11:15:46.000Z","updated_at":"2026-01-13T21:19:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/leodiegues/unwrappy","commit_stats":null,"previous_names":["leodiegues/unwrappy"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/leodiegues/unwrappy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leodiegues%2Funwrappy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leodiegues%2Funwrappy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leodiegues%2Funwrappy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leodiegues%2Funwrappy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leodiegues","download_url":"https://codeload.github.com/leodiegues/unwrappy/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leodiegues%2Funwrappy/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28506593,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T10:25:30.148Z","status":"ssl_error","status_checked_at":"2026-01-17T10:25:29.718Z","response_time":85,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","either-type","error-handling","functional-programming","maybe-type","monads","option-type","pattern-matching","python","railway-oriented-programming","result-type","rust","rust-inspired","type-hints","type-safe"],"created_at":"2026-01-17T10:47:32.622Z","updated_at":"2026-01-17T10:47:33.282Z","avatar_url":"https://github.com/leodiegues.png","language":"Python","readme":"# unwrappy\n\nRust-inspired `Result` and `Option` types for Python, enabling safe, expressive error handling with errors as values.\n\n## Installation\n\n```bash\npip install unwrappy\n```\n\n## Quick Start\n\n```python\nfrom unwrappy import Ok, Err, Result\n\ndef divide(a: int, b: int) -\u003e Result[float, str]:\n    if b == 0:\n        return Err(\"division by zero\")\n    return Ok(a / b)\n\n# Pattern matching (Python 3.10+)\nmatch divide(10, 2):\n    case Ok(value):\n        print(f\"Result: {value}\")\n    case Err(error):\n        print(f\"Error: {error}\")\n\n# Combinator chaining\nresult = (\n    divide(10, 2)\n    .map(lambda x: x * 2)\n    .and_then(lambda x: Ok(int(x)) if x \u003c 100 else Err(\"too large\"))\n)\n```\n\n## Why unwrappy?\n\n- **Explicit error handling**: No hidden exceptions, errors are values\n- **Type-safe**: Full generic type support with proper inference\n- **Functional**: Rich combinator API (map, and_then, or_else, etc.)\n- **Async-first**: LazyResult for clean async operation chaining\n- **Pattern matching**: Works with Python 3.10+ structural matching\n\n## Core Types\n\n### Result[T, E]\n\nA type that represents either success (`Ok`) or failure (`Err`).\n\n```python\nfrom unwrappy import Ok, Err, Result\n\n# Success\nok: Result[int, str] = Ok(42)\nok.unwrap()      # 42\nok.is_ok()       # True\n\n# Error\nerr: Result[int, str] = Err(\"failed\")\nerr.unwrap_err() # \"failed\"\nerr.is_err()     # True\n```\n\n### LazyResult[T, E]\n\nFor async operation chaining without nested awaits:\n\n```python\nfrom unwrappy import LazyResult, Ok, Err\n\nasync def fetch_user(id: int) -\u003e Result[dict, str]: ...\nasync def fetch_profile(user: dict) -\u003e Result[dict, str]: ...\n\n# Clean async chaining - no nested awaits!\nresult = await (\n    LazyResult.from_awaitable(fetch_user(42))\n    .and_then(fetch_profile)\n    .map(lambda p: p[\"name\"])\n    .map(str.upper)\n    .collect()\n)\n```\n\n### Option[T]\n\nA type that represents an optional value: either `Some(value)` or `Nothing`.\n\n```python\nfrom unwrappy import Some, NOTHING, Option, from_nullable\n\n# Has value\nsome: Option[int] = Some(42)\nsome.unwrap()      # 42\nsome.is_some()     # True\n\n# No value\nnothing: Option[int] = NOTHING\nnothing.is_nothing()  # True\n\n# From nullable Python value\nvalue: str | None = get_optional_value()\nopt = from_nullable(value)  # Some(value) or NOTHING\n```\n\n### LazyOption[T]\n\nFor async operation chaining on optional values:\n\n```python\nfrom unwrappy import LazyOption, Some\n\nasync def fetch_config(key: str) -\u003e Option[str]: ...\nasync def parse_value(s: str) -\u003e Option[int]: ...\n\n# Clean async chaining\nresult = await (\n    LazyOption.from_awaitable(fetch_config(\"timeout\"))\n    .and_then(parse_value)\n    .map(lambda x: x * 1000)\n    .collect()\n)\n```\n\n## API Overview\n\n### Result API\n\n#### Transformation\n\n| Method | Description |\n|--------|-------------|\n| `map(fn)` | Transform Ok value |\n| `map_err(fn)` | Transform Err value |\n| `and_then(fn)` | Chain Result-returning function |\n| `or_else(fn)` | Recover from Err |\n\n#### Extraction\n\n| Method | Description |\n|--------|-------------|\n| `unwrap()` | Get value or raise UnwrapError |\n| `unwrap_or(default)` | Get value or default |\n| `unwrap_or_else(fn)` | Get value or compute default |\n| `unwrap_or_raise(fn)` | Get value or raise custom exception from fn(error) |\n| `expect(msg)` | Get value or raise with message |\n\n#### Inspection\n\n| Method | Description |\n|--------|-------------|\n| `is_ok()` / `is_err()` | Check variant |\n| `ok()` / `err()` | Convert to Option |\n| `tee(fn)` / `inspect(fn)` | Side effect on Ok |\n| `inspect_err(fn)` | Side effect on Err |\n\n#### Utilities\n\n| Function/Method | Description |\n|-----------------|-------------|\n| `flatten()` | Unwrap nested Result |\n| `split()` | Convert to (value, error) tuple |\n| `filter(predicate, error)` | Keep Ok if predicate passes |\n| `zip(other)` / `zip_with(other, fn)` | Combine two Results |\n| `context(error)` | Add context to errors |\n| `sequence_results(results)` | Collect Results into Result |\n| `traverse_results(items, fn)` | Map and collect |\n\n### Option API\n\n#### Transformation\n\n| Method | Description |\n|--------|-------------|\n| `map(fn)` | Transform Some value |\n| `map_or(default, fn)` | Transform or return default |\n| `map_or_else(default_fn, fn)` | Transform or compute default |\n| `and_then(fn)` | Chain Option-returning function |\n| `or_else(fn)` | Recover from Nothing |\n| `filter(predicate)` | Keep value if predicate passes |\n\n#### Extraction\n\n| Method | Description |\n|--------|-------------|\n| `unwrap()` | Get value or raise UnwrapError |\n| `unwrap_or(default)` | Get value or default |\n| `unwrap_or_else(fn)` | Get value or compute default |\n| `unwrap_or_raise(exc)` | Get value or raise exception |\n| `expect(msg)` | Get value or raise with message |\n\n#### Inspection\n\n| Method | Description |\n|--------|-------------|\n| `is_some()` / `is_nothing()` | Check variant |\n| `tee(fn)` / `inspect(fn)` | Side effect on Some |\n| `inspect_nothing(fn)` | Side effect on Nothing |\n\n#### Utilities\n\n| Function/Method | Description |\n|-----------------|-------------|\n| `from_nullable(value)` | Convert None to Nothing |\n| `flatten()` | Unwrap nested Option |\n| `zip(other)` / `zip_with(other, fn)` | Combine two Options |\n| `xor(other)` | Exactly one must be Some |\n| `ok_or(err)` / `ok_or_else(fn)` | Convert to Result |\n| `to_tuple()` | Convert to single-element tuple |\n| `sequence_options(options)` | Collect Options into Option |\n| `traverse_options(items, fn)` | Map and collect |\n\n## Examples\n\n### Error Recovery\n\n```python\ndef get_config(key: str) -\u003e Result[str, str]:\n    return Err(f\"missing: {key}\")\n\n# Recover with default\nvalue = get_config(\"port\").unwrap_or(\"8080\")\n\n# Recover with computation\nvalue = (\n    get_config(\"port\")\n    .or_else(lambda e: Ok(\"8080\"))\n    .unwrap()\n)\n```\n\n### Chaining Operations\n\n```python\ndef parse_int(s: str) -\u003e Result[int, str]:\n    try:\n        return Ok(int(s))\n    except ValueError:\n        return Err(f\"invalid number: {s}\")\n\ndef validate_positive(n: int) -\u003e Result[int, str]:\n    return Ok(n) if n \u003e 0 else Err(\"must be positive\")\n\nresult = (\n    parse_int(\"42\")\n    .and_then(validate_positive)\n    .map(lambda x: x * 2)\n)\n# Ok(84)\n```\n\n### Async Operations with LazyResult\n\n```python\nasync def fetch_user(id: int) -\u003e Result[User, str]:\n    # async database call\n    ...\n\nasync def fetch_posts(user: User) -\u003e Result[list[Post], str]:\n    # async API call\n    ...\n\n# Build pipeline, execute once\nresult = await (\n    LazyResult.from_awaitable(fetch_user(42))\n    .and_then(fetch_posts)              # async\n    .map(lambda posts: len(posts))      # sync\n    .tee(lambda n: print(f\"Found {n}\")) # side effect\n    .collect()\n)\n```\n\n### Working with Optional Values\n\n```python\nfrom unwrappy import Some, NOTHING, Option, from_nullable\n\n# Convert nullable Python values\ndef get_user_email(user_id: int) -\u003e str | None:\n    # May return None if user has no email\n    ...\n\nemail_opt = from_nullable(get_user_email(42))\n\n# Chain operations on optional values\ndisplay_name = (\n    email_opt\n    .map(lambda e: e.split(\"@\")[0])\n    .map(str.title)\n    .unwrap_or(\"Anonymous\")\n)\n\n# Filter with predicates\nvalid_port = (\n    Some(8080)\n    .filter(lambda p: 1 \u003c= p \u003c= 65535)\n    .unwrap_or(3000)\n)\n\n# Convert to Result for error context\nresult = (\n    from_nullable(get_user_email(42))\n    .ok_or(\"User has no email configured\")\n)\n```\n\n### Batch Processing\n\n```python\nfrom unwrappy import Ok, sequence_results, traverse_results\n\n# Collect multiple Results\nresults = [Ok(1), Ok(2), Ok(3)]\ncombined = sequence_results(results)  # Ok([1, 2, 3])\n\n# Map and collect\nitems = [\"1\", \"2\", \"3\"]\nparsed = traverse_results(items, parse_int)  # Ok([1, 2, 3])\n```\n\n```python\nfrom unwrappy import Some, NOTHING, sequence_options, traverse_options, from_nullable\n\n# Collect multiple Options\noptions = [Some(1), Some(2), Some(3)]\ncombined = sequence_options(options)  # Some([1, 2, 3])\n\n# Fails fast if any is Nothing\noptions_with_nothing = [Some(1), NOTHING, Some(3)]\ncombined = sequence_options(options_with_nothing)  # NOTHING\n\n# Map nullable values and collect\nitems: list[int | None] = [1, 2, 3]\nresult = traverse_options(items, from_nullable)  # Some([1, 2, 3])\n```\n\n## Serialization\n\nunwrappy supports JSON serialization for integration with task queues and workflow frameworks (Celery, Temporal, DBOS, etc.).\n\n```python\nfrom unwrappy import Ok, Err, Some, NOTHING, dumps, loads\n\n# Serialize Result\nencoded = dumps(Ok({\"key\": \"value\"}))\n# '{\"__unwrappy_type__\": \"Ok\", \"value\": {\"key\": \"value\"}}'\n\n# Serialize Option\nencoded = dumps(Some(42))\n# '{\"__unwrappy_type__\": \"Some\", \"value\": 42}'\n\nencoded = dumps(NOTHING)\n# '{\"__unwrappy_type__\": \"Nothing\"}'\n\n# Deserialize\ndecoded = loads(encoded)  # Some(42), NOTHING, Ok(...), or Err(...)\n```\n\nFor standard json module usage:\n\n```python\nimport json\nfrom unwrappy import ResultEncoder, result_decoder\n\nencoded = json.dumps(Ok(42), cls=ResultEncoder)\ndecoded = json.loads(encoded, object_hook=result_decoder)\n```\n\n\u003e **Note**: `LazyResult` and `LazyOption` cannot be serialized directly. Call `.collect()` first to get a concrete `Result` or `Option`.\n\nSee [ARCHITECTURE.md](ARCHITECTURE.md#serialization-support) for framework integration examples.\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleodiegues%2Funwrappy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleodiegues%2Funwrappy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleodiegues%2Funwrappy/lists"}