{"id":45403525,"url":"https://github.com/overflowy/safe-result","last_synced_at":"2026-02-21T20:37:05.010Z","repository":{"id":284021928,"uuid":"953439469","full_name":"overflowy/safe-result","owner":"overflowy","description":"A Python package for elegant error handling, inspired by Rust's Result type.","archived":false,"fork":false,"pushed_at":"2025-11-24T09:22:19.000Z","size":59,"stargazers_count":160,"open_issues_count":0,"forks_count":5,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-12-25T11:45:37.167Z","etag":null,"topics":["error-handling","exception-handling","python","result-type","type-safety"],"latest_commit_sha":null,"homepage":"https://deepwiki.com/overflowy/safe-result","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/overflowy.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":"2025-03-23T11:25:37.000Z","updated_at":"2025-12-19T15:43:05.000Z","dependencies_parsed_at":"2025-03-23T18:42:59.203Z","dependency_job_id":null,"html_url":"https://github.com/overflowy/safe-result","commit_stats":null,"previous_names":["overflowy/safe-result"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/overflowy/safe-result","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fsafe-result","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fsafe-result/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fsafe-result/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fsafe-result/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/overflowy","download_url":"https://codeload.github.com/overflowy/safe-result/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/overflowy%2Fsafe-result/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29692521,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T18:18:25.093Z","status":"ssl_error","status_checked_at":"2026-02-21T18:18:22.435Z","response_time":107,"last_error":"SSL_read: 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":["error-handling","exception-handling","python","result-type","type-safety"],"created_at":"2026-02-21T20:37:04.447Z","updated_at":"2026-02-21T20:37:05.003Z","avatar_url":"https://github.com/overflowy.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# safe-result\n\nA Python package for elegant error handling, inspired by Rust's Result type.\n\n## Installation\n\n```bash\npip install safe-result\n```\n\n## Overview\n\n`safe-result` provides type-safe objects that represent either success (`Ok`) or failure (`Err`). This approach enables more explicit error handling without relying on try/catch blocks, making your code more predictable and easier to reason about.\n\nKey features:\n\n- 100% test coverage\n- Type-safe result handling with full generics support\n- Pattern matching support for elegant error handling\n- Type guards for safe access and type narrowing\n- Decorators to automatically wrap function returns in `Result` objects\n- Methods for transforming and chaining results (`map`, `map_async`, `and_then`, `and_then_async`, `flatten`)\n- Methods for accessing values, providing defaults or propagating errors within a `@safe` context\n- Handy traceback capture for comprehensive error information\n\n## Usage\n\n### Basic Usage\n\nCreate `Result` objects directly or use the provided decorators.\n\n```python\nfrom safe_result import Err, Ok, Result, ok\n\ndef divide(a: int, b: int) -\u003e Result[float, ZeroDivisionError]:\n    if b == 0:\n        return Err(ZeroDivisionError(\"Cannot divide by zero\"))  # Failure case\n    return Ok(a / b)  # Success case\n\n# Function signature clearly communicates potential failure modes\nfoo = divide(10, 0)  # -\u003e Result[float, ZeroDivisionError]\n\n# Type checking will prevent unsafe access to the value\nbar = 1 + foo.value\n#         ^^^^^^^^^ Type checker indicates the error:\n# \"Operator '+' not supported for types 'Literal[1]' and 'float | None'\"\n\n# Safe access pattern using the type guard function\nif ok(foo):  # Verifies foo is an Ok result and enables type narrowing\n    bar = 1 + foo.value  # Safe! Type checker knows the value is a float here\nelse:\n    # Handle error case with full type information about the error\n    print(f\"Error: {foo.error}\")\n\n# Pattern matching is also a great way to handle results\nmatch foo:\n    case Ok(value):\n        print(f\"Success: {value}\")\n    case Err(ZeroDivisionError() as e):\n        print(f\"Division Error: {e}\")\n```\n\n### Using Decorators\n\nDecorators simplify wrapping existing functions.\n\n**`@safe`**: Catches _any_ `Exception` and returns `Result[ReturnType, Exception]`.\n\n```python\nfrom safe_result import ok, safe\n\n@safe\ndef may_fail(data: str) -\u003e int:\n    return int(data)\n\ndef do_something():\n    result1 = may_fail(\"123\")  # -\u003e Ok(123)\n    result2 = may_fail(\"abc\")  # -\u003e Err(ValueError(\"invalid literal for int() with base 10: 'abc'\"))\n\n    if ok(result1):\n        do_something_else(result1.value)\n    else:\n        print(f\"Caught error: {result1.error}\")\n        return result1\n\n    # Or even better by inverting the condition\n    if not ok(result2):\n        print(f\"Caught error: {result2.error}\")\n        return result2\n\n    # Continue with the rest of the function\n    do_something_else(result2.value)\n```\n\n**`@safe_with(*ExceptionTypes)`**: Catches only the specified exception types, returning `Result[ReturnType, Union[ExceptionTypes]]`. Other exceptions are raised normally.\n\n```python\nfrom typing import Any\nfrom safe_result import err_type, safe_with\n\n@safe_with(ValueError, TypeError)\ndef process_input(data: Any) -\u003e str:\n    if not isinstance(data, str):\n        raise TypeError(\"Input must be a string\")\n    if not data:\n        raise ValueError(\"Input cannot be empty\")\n    return f\"Processed: {data}\"\n\nres1 = process_input(\"hello\")  # -\u003e Ok('Processed: hello')\nres2 = process_input(\"\")       # -\u003e Err(ValueError('Input cannot be empty'))\nres3 = process_input(123)      # -\u003e Err(TypeError('Input must be a string'))\nres4 = process_input(None)     # -\u003e Raises TypeError (caught by decorator)\n\n# Use err_type for specific error handling\nif err_type(res2, ValueError):\n    print(\"ValueError occurred!\")\n```\n\n### Async Support\n\n`@safe_async` and `@safe_async_with` work identically for asynchronous functions. `asyncio.CancelledError` is never caught and always re-raised.\n\n```python\nimport asyncio\nfrom safe_result import Err, Ok, safe_async, safe_async_with\n\n@safe_async\nasync def fetch_data(url: str) -\u003e str:\n    await asyncio.sleep(0.1)  # Simulate network\n    if \"invalid\" in url:\n        raise ValueError(\"Invalid URL\")\n    return f\"Data from {url}\"\n\n@safe_async_with(ConnectionError)\nasync def fetch_specific(url: str) -\u003e str:\n    await asyncio.sleep(0.1)\n    if \"timeout\" in url:\n        raise ConnectionError(\"Timeout\")\n    return f\"Data from {url}\"\n\nasync def main():\n    result1 = await fetch_data(\"valid-url\")    # -\u003e Ok('Data from valid-url')\n    result2 = await fetch_data(\"invalid-url\")  # -\u003e Err(ValueError('Invalid URL'))\n    result3 = await fetch_specific(\"ok\")       # -\u003e Ok('Data from ok')\n    result4 = await fetch_specific(\"timeout\")  # -\u003e Err(ConnectionError('Timeout'))\n    # result5 = await fetch_specific(123)      # -\u003e Raises TypeError (not caught)\n\n    # Handle the result\n    match result1:\n        case Ok(v):\n            print(f\"Fetched data: {v}\")\n        case Err(ValueError() as e):\n            print(f\"Invalid URL error: {e}\")\n        case Err(e):\n            print(f\"Some other error occurred with fetch_data: {e}\")\n```\n\n### Working with Results\n\n`Ok` and `Err` provide methods for transforming and accessing the contained values.\n\n**`unwrap()`**: Returns the value if `Ok`, otherwise raises the contained error. Use cautiously, often within functions already decorated with `@safe` variants for automatic error propagation.\n\n```python\nfrom safe_result import Err, Ok, Result, safe\n\nok_res = Ok(42)\nerr_res = Err(ValueError(\"Bad data\"))\n\nprint(ok_res.unwrap())  # -\u003e 42\n# err_res.unwrap()      # -\u003e Raises ValueError: Bad data\n\n@safe\ndef combined_op(res1: Result[int, Exception], res2: Result[int, Exception]) -\u003e int:\n    # unwrap() propagates errors automatically within @safe context\n    val1 = res1.unwrap()\n    val2 = res2.unwrap()\n    return val1 + val2\n\nprint(combined_op(Ok(10), Ok(5)))                    # -\u003e Ok(15)\nprint(combined_op(Ok(10), Err(ValueError(\"Fail\"))))  # -\u003e Err(ValueError('Fail'))\n```\n\n**`unwrap_or(default)`**: Returns the value if `Ok`, otherwise returns the `default` value.\n\n```python\nprint(Ok(42).unwrap_or(0))        # -\u003e 42\nprint(Err(\"Error\").unwrap_or(0))  # -\u003e 0\n```\n\n**`map(func)`**: Applies `func` to the value if `Ok`, returns a new `Ok` with the result. If `Err`, returns the original `Err` unchanged.\n\n```python\nprint(Ok(5).map(lambda x: x * 2))        # -\u003e Ok(10)\nprint(Err(\"Fail\").map(lambda x: x * 2))  # -\u003e Err('Fail')\n```\n\n**`map_async(async_func)`**: Applies `async_func` if `Ok`. Returns `await Ok(await async_func(value))`. If `Err`, returns the original `Err`.\n\n```python\nasync def double_async(x):\n    await asyncio.sleep(0)\n    return x * 2\n\nasync def run_map_async():\n    print(await Ok(5).map_async(double_async))        # -\u003e Ok(10)\n    print(await Err(\"Fail\").map_async(double_async))  # -\u003e Err('Fail')\n```\n\n**`and_then(func)`**: Calls `func` with the value if `Ok`. `func` _must_ return a `Result`. Useful for chaining operations that can fail. If `Err`, returns the original `Err`.\n\n```python\ndef check_positive(n): return Ok(n) if n \u003e 0 else Err(\"Not positive\")\n\nprint(Ok(5).and_then(check_positive))        # -\u003e Ok(5)\nprint(Ok(-1).and_then(check_positive))       # -\u003e Err('Not positive')\nprint(Err(\"Fail\").and_then(check_positive))  # -\u003e Err('Fail')\n```\n\n**`and_then_async(async_func)`**: Calls `async_func` with the value if `Ok`. `async_func` _must_ return an `Awaitable[Result]`. If `Err`, returns the original `Err`.\n\n```python\nasync def check_positive_async(n):\n    await asyncio.sleep(0)\n    return Ok(n) if n \u003e 0 else Err(\"Not positive async\")\n\nasync def run_and_then_async():\n    print(await Ok(5).and_then_async(check_positive_async))        # -\u003e Ok(5)\n    print(await Ok(-1).and_then_async(check_positive_async))       # -\u003e Err('Not positive async')\n    print(await Err(\"Fail\").and_then_async(check_positive_async))  # -\u003e Err('Fail')\n```\n\n**`flatten()`**: Converts `Result[Result[T, E], E]` to `Result[T, E]`. Flattens nested `Ok(Ok(value))` to `Ok(value)` and `Ok(Err(error))` to `Err(error)`. Has no effect on non-nested `Result` or `Err`.\n\n```python\nprint(Ok(Ok(42)).flatten())        # -\u003e Ok(42)\nprint(Ok(Err(\"Inner\")).flatten())  # -\u003e Err('Inner')\nprint(Err(\"Outer\").flatten())      # -\u003e Err('Outer')\nprint(Ok(10).flatten())            # -\u003e Ok(10)\n```\n\n### Helper Functions\n\n**`err_type(result, ExceptionType)`**: Type guard that checks if a `Result` is an `Err` containing a specific exception type (or subtype).\n\n```python\nfrom safe_result import err_type\n\nresult = Err(ValueError(\"Invalid input\"))\n\nif err_type(result, ValueError):\n    print(\"It's a ValueError!\")  # -\u003e True\nif err_type(result, TypeError):\n    print(\"It's a TypeError!\")   # -\u003e False (doesn't print)\nif err_type(result, Exception):\n    print(\"It's an Exception!\")  # -\u003e True\n```\n\n**`traceback_of(result)`**: Returns the formatted traceback string if the `Result` is an `Err` containing an `Exception`, otherwise returns an empty string.\n\n```python\nfrom safe_result import safe, traceback_of\n\n@safe\ndef cause_error():\n    return 1 / 0\n\nerror_result = cause_error()  # -\u003e Err(ZeroDivisionError('division by zero'))\n\nif not ok(error_result):\n    tb = traceback_of(error_result)\n    print(f\"Error occurred:\\n{tb}\")\n    # Prints the full traceback leading to the ZeroDivisionError\n```\n\n### Real-world example\n\nHere's a practical example using `httpx` for HTTP requests with proper error handling:\n\n```python\nimport asyncio\nimport httpx\nfrom safe_result import safe_async_with, Ok, Err, err_type, traceback_of\n\n# Only catch specific network/HTTP errors\n@safe_async_with(httpx.TimeoutException, httpx.HTTPStatusError, ConnectionError)\nasync def fetch_api_data(url: str, timeout: float = 5.0) -\u003e dict:\n    async with httpx.AsyncClient() as client:\n        response = await client.get(url, timeout=timeout)\n        response.raise_for_status()  # Raises HTTPStatusError for 4XX/5XX responses\n        return response.json()\n\nasync def main():\n    # Example with timeout\n    result_timeout = await fetch_api_data(\"https://httpbin.org/delay/10\", timeout=2.0)\n    match result_timeout:\n        case Ok(data):\n            print(f\"Data received: {data}\")\n        case Err(httpx.TimeoutException):\n            print(\"Request timed out - the server took too long to respond\")\n        case Err(httpx.HTTPStatusError as e):\n            print(f\"HTTP Error: {e.response.status_code} for URL: {e.request.url}\")\n        case Err(e):  # Catch other specified errors like ConnectionError\n             print(f\"Network error: {e}\")\n             print(traceback_of(result_timeout))  # Print traceback for unexpected errors\n\n    # Example with success\n    result_ok = await fetch_api_data(\"https://httpbin.org/json\")\n    if ok(result_ok):\n        print(f\"Successfully fetched JSON data: {result_ok.value.get('slideshow', {}).get('title')}\")\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foverflowy%2Fsafe-result","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foverflowy%2Fsafe-result","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foverflowy%2Fsafe-result/lists"}