{"id":21933151,"url":"https://github.com/olekli/drresult","last_synced_at":"2025-08-08T18:55:25.655Z","repository":{"id":257946656,"uuid":"861975649","full_name":"olekli/DrResult","owner":"olekli","description":"More radical approach to Rust's `std::result` in Python.","archived":false,"fork":false,"pushed_at":"2024-10-17T23:51:36.000Z","size":74,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-14T19:51:06.729Z","etag":null,"topics":["error-handling","error-handling-python","exception-handling","exceptionless","exceptions","exceptions-and-error-handling","python","result","rust-like","rust-result","typed"],"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/olekli.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}},"created_at":"2024-09-23T20:12:23.000Z","updated_at":"2024-10-17T23:51:32.000Z","dependencies_parsed_at":"2024-10-20T01:52:25.870Z","dependency_job_id":null,"html_url":"https://github.com/olekli/DrResult","commit_stats":null,"previous_names":["olekli/drresult"],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olekli%2FDrResult","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olekli%2FDrResult/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olekli%2FDrResult/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olekli%2FDrResult/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/olekli","download_url":"https://codeload.github.com/olekli/DrResult/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244960898,"owners_count":20538898,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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","error-handling-python","exception-handling","exceptionless","exceptions","exceptions-and-error-handling","python","result","rust-like","rust-result","typed"],"created_at":"2024-11-29T00:08:20.450Z","updated_at":"2025-03-22T13:24:11.603Z","avatar_url":"https://github.com/olekli.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"![tests](https://github.com/olekli/drresult/actions/workflows/pytest-coverage.yml/badge.svg)\n[![codecov](https://codecov.io/github/olekli/DrResult/branch/main/graph/badge.svg?token=WLSOABF6I6)](https://codecov.io/github/olekli/DrResult)\n[![Documentation Status](https://readthedocs.org/projects/drresult/badge/?version=latest)](https://drresult.readthedocs.io/en/latest/?badge=latest)\n[![PyPI version](https://badge.fury.io/py/DrResult.svg)](https://badge.fury.io/py/DrResult)\n\n# DrResult\n\nMore radical approach to Rust's `std::result` in Python.\n\n## Motivation\n\nI do not want exceptions in my code.\nRust has this figured out quite neatly\nby essentially revolving around two pathways for errors:\nA possible error condition is either one that has no prospect of being handled\n-- then the program should terminate -- or it is one that could be handled --\nthen it has to be handled or explicitly ignored.\n\nThis concept is replicated here by using mapping all unhandled exceptions to `Panic`\nand providing a Rust-like `result` type to signal error conditions that do not need to terminate\nthe program.\n\n## Documentation\n\n### Concept\n\nAt each point in your code, there are exceptions that are considered to be expected\nand there are exceptions that are considered unexpected.\n\nIn general, an unexpected exception will be mapped to `Panic`.\nNo part of DrResult will attempt to handle a `Panic` exception.\nAnd you should leave all exception handling to DrResult,\ni.e. have no `try/except` blocks in your code.\nIn any case, you should never catch `Panic`.\nAn unexpected exception will therefore result in program termination with stack unwinding.\n\nYou will need to specify which exceptions are to be expected.\nThere are two modes of operation here:\nYou can explicitly name the exceptions to be expected in a function.\nOr you can skip that and basically expect all exceptions.\n\n_Basically_ means: By default only `Exception` is expected, not `BaseException`.\nAnd even of type `Exception` that are some considered to be never expected:\n```python\nAssertionError, AttributeError, ImportError, MemoryError, NameError, SyntaxError, SystemError, TypeError\n```\nIf you do not explicitly expect these, they will be implicitly unexpected.\n(Obviously, the exact list may be up for debate.)\n\n### Basic Usage\n\n#### `@noexcept`\n\nIf your function knows no errors, you mark it as `@noexcept()`:\n\n```python\nfrom drresult import noexcept\n\n@noexcept()\ndef sum(a: list) -\u003e int:\n    result = 0\n    for item in a:\n        result += item\n    return result\n\nresult = func([1, 2, 3])\n```\n\nThis will do what you expect it does.\nBut if you screw up...\n```python\n@noexcept()\ndef sum(a: list) -\u003e int:\n    result = 0\n    for item in a:\n        result += item\n    print(a[7])   # IndexError\n    return result\n\nresult = func([1, 2, 3])    # Panic!\n```\n... then it will raise `Panic` preserving the stack trace and the original exception.\n\nThis way all unexpected exceptions are normalised to `Panic`.\n\nPlease note that a `@noexecpt` function does not return a result but just the return type itself.\n\n#### `@returns_result()` and `expects`\n\nMarking a function as `@returns_result` will wrap any exceptions thrown in an `Err` result.\nBut only those exceptions that are expected.\nAs noted above, if you do not explicitly specify exceptions to expect,\nmost runtime exceptions are expected by default.\n\n```python\n@returns_result()\ndef read_file() -\u003e Result[str]:\n    with open('/some/path/that/might/be/invalid') as f:\n        return Ok(f.read())\n\nresult = read_file()\nif result.is_ok():\n    print(f'File content: {result.unwrap()}')\nelse:\n    print(f'Error: {str(result.unwrap_err())}')\n```\n\nThis will do as you expect.\n\nYou can also explicitly specify the exception to expect:\n```python\n@returns_result(expects=[FileNotFoundError])\ndef read_file() -\u003e Result[str]:\n    with open('/some/path/that/might/be/invalid') as f:\n        return Ok(f.read())\n\nresult = read_file()\nif result.is_ok():\n    print(f'File content: {result.unwrap()}')\nelse:\n    print(f'Error: {str(result.unwrap_err())}')\n```\nThis also will do as you expect.\n\nIf fail to specify an exception that is raised as expected...\n```python\nfrom drresult import returns_result\n\n@returns_result(expects=[IndexError, KeyError])\ndef read_file() -\u003e Result[str]:\n    with open('/this/path/is/invalid') as f:\n        return Ok(f.read())\n\nresult = read_file()    # Panic!\n```\n.. it will be re-raised as `Panic`.\n\nIf you are feeling fancy, you can also do pattern matching:\n```python\n@returns_result(expects=[FileNotFoundError])\ndef read_file() -\u003e Result[str]:\n    with open('/this/path/is/invalid') as f:\n        return Ok(f.read())\n\nresult = read_file()\nmatch result:\n    case Ok(v):\n        print(f'File content: {v}')\n    case Err(e):\n        print(f'Error: {e}')\n```\n\nAnd even fancier:\n```python\ndata = [{ 'foo': 'value-1' }, { 'bar': 'value-2' }]\n\n@returns_result(expects=[IndexError, KeyError, RuntimeError])\ndef retrieve_record_entry_backend(index: int, key: str) -\u003e Result[str]:\n    if key == 'baz':\n        raise RuntimeError('Cannot process baz!')\n    return Ok(data[index][key])\n\ndef retrieve_record_entry(index: int, key: str):\n    match retrieve_record_entry_backend(index: int, key: str):\n        case Ok(v):\n            print(f'Retrieved: {v}')\n        case Err(IndexError()):\n            print(f'No such record: {index}')\n        case Err(KeyError()):\n            print(f'No entry `{key}` in record {index}')\n        case Err(RuntimeError() as e):\n            print(f'Error: {e}')\n\nretrieve_record_entry(2, 'foo')    # No such record: 2\nretrieve_record_entry(1, 'foo')    # No entry `foo` in record 1\nretrieve_record_entry(1, 'bar')    # Retrieved: value-2\nretrieve_record_entry(1, 'baz')    # Error: Cannot process baz!\n```\n\n\n#### Implicit conversion to bool\n\nIf you are feeling more lazy than fancy, you can do this:\n```python\nresult = Ok('foo')\nassert result\n\nresult = Err('bar')\nassert not result\n```\n\n\n#### `unwrap_or_raise`\n\nYou can replicate the behaviour of Rust's `?`-operator with `unwrap_or_raise`:\n```python\n@returns_result()\ndef read_json(filename: str) -\u003e Result[str]:\n    with open(filename) as f:\n        return Ok(json.loads(f.read()))\n\n@returns_result()\ndef parse_file(filename: str) -\u003e Result[dict]:\n    content = read_file(filename).unwrap_or_raise()\n    if not 'required_key' in content:\n        raise KeyError('required_key')\n    return Ok(content)\n```\nIf the result is not `Ok`, `unwrap_or_raise()` will re-raise the contained exception.\nObviously, this will lead to an assertion if the contained exception is not expected.\n\n\n#### `gather_result`\n\nWhen you are interfacing with other modules that use exceptions,\nyou may want to react to certain exceptions being raised.\nTo avoid having to use `try/except` again,\nyou can transform exceptions from a part of your code to results:\n\n```python\n@returns_result()\ndef parse_json_file(filename: str) -\u003e Result[dict]:\n    with gather_result() as result:\n        with open(filename) as f:\n            result.set(Ok(json.loads(f.read())))\n    result = result.get()\n    match result:\n        case Ok(data):\n            return Ok(data)\n        case Err(FileNotFoundError()):\n            return Ok({})\n        case _:\n            return result\n```\n\n#### Printing the Stack Trace\n\nIf you want to format the exception stored in `Err`,\nyou can use `Err.__str__()` and `Err.trace()`.\nThe former will just provide the error message itself\nwhere the latter will provide the entire stack trace.\nThe trace is filtered to remove all intermediate frames for internal functions.\n\nAlso, DrResult overrides the `expecthook` to filter the stack trace in case of panic.\n\n#### `constructs_as_result`\n\nIf you have a class that might raise an error in its constructor,\nyou can mark it as `constructs_as_result`:\n\n```python\n@constructs_as_result\nclass Reader:\n    def __init__(self, filename):\n        with open(filename) as f:\n            self.data = json.loads(f.read())\n```\nCreating an instance of this class will yield a `Result` that has to be unwrapped first.\n```python\nreader = Reader('/path/to/existing/file').unwrap() # Ok\nreader = Reader('/invalid/path/to/non/existing/file').unwrap() # panic!\n```\n\n## Similar Projects\n\nFor a less extreme approach on Rust's result type, see:\n\n* [https://github.com/rustedpy/result](https://github.com/rustedpy/result)\n* [https://github.com/felixhammerl/resultify](https://github.com/felixhammerl/resultify)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folekli%2Fdrresult","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Folekli%2Fdrresult","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folekli%2Fdrresult/lists"}