Package: explicitor
ExplicitorRust-like explicit errors for typed Python.Why this existsExceptions suck in many statically typed programming languages.
From a function signature it is usually impossible to know whether the function can throw an error.
The Rust programming language solves this problem quite elegantly with theResult enum, which allows you to use the type checker to reason about correct error handling.
An additional benefit is that error handling is much more elegant thantry ... exceptclauses.The goal of this package is to emulate in Python the Rust way of dealing with errors.
The implementation isheavilyinspired by the contents ofthis blogpostbyJustin Ellis.
The philosophy is that instead of raising exceptions, exceptions are returned wrapped in anErrclass, which is a variant of theResulttype.
Hence, a function that can get into an error state returns aResultwhich can be clearly indicated in a function signature.
This can be valuable when one uses static type analysis tools likemypy.InstallationpipinstallexplicitorHow to use itThere are 3 types defined:Ok,Errwhich are the two variants ofResult.Instead of defining a function that can raise an exception, for example:deffunction(x:int)->int:ifx<10:raiseValueError("too small")returnx+1you would define your function asfromexplicitorimportOk,Err,Resultdeffunction(x:int)->Result[int,ValueError]:ifx<10:returnErr(ValueError("too small"))returnOk(x+1)To deal with the error, there's a number of methods implemented onErrandOk:result_1=function(20)# should be an Okresult_2=function(5)# should be an Err# unwrap: returns the wrapped value of an Ok or raises the error contained by an Errassertresult_1.unwrap()==21try:result_2.unwrap()exceptValueErrorase:assertstr(e)=="too small"# expect: same as unwrap, but raises a custom exception with custom messageassertresult_1.expect("bla")==21try:result_2.expect("bla")exceptExceptionase:assertstr(e)=="bla"# is_ok and is_err: checks what variant it is and returns a booleanassertresult_1.is_ok()assertresult_2.is_err()# unwrap_or: if Ok, unwrap, if Err, return the value suppliedassertresult_1.unwrap_or(2)==21assertresult_2.unwrap_or(2)==2# unwrap_or_else: if Ok, unwrap, if Err, apply a function to the wrapped error and return itassertresult_1.unwrap_or_else(lambdax:str(x))==21assertresult_2.unwrap_or_else(lambdax:str(x))=="too small"# unwrap_err: the opposite of unwrap, raise an exception if Ok, return the wrapped Exception if Errtry:result_1.unwrap_err()exceptExceptionase:assertstr(e)=="Unexpected Ok(21)"assertstr(result_2.unwrap_err())=="too small"# expect_err: the opposite of expect, raise an exception with custom message if Ok, return the wrapped Exception if Errtry:result_1.expect_err("custom message")exceptExceptionase:assertstr(e)=="custom message"assertstr(result_2.expect_err("custom message"))=="too small"# map: apply a function to the wrapped value in Ok, do nothing if Errassertresult_1.map(lambdax:x+1)==Ok(22)assertresult_2.map(lambdax:x+1)==Err(ValueError("too small"))Finally, in Python 3.10+, it's possible to use Rust-likematchstatements for dealing with the errors:matchfunction(20):caseOk(v):print("The value is",v)caseErr(e):print("The error is",str(e))Suppose a function does raise exceptions, it is possible to use theunraisedecorator to catch these exceptions and return them wrapped in anErrinstead.
If no exception is raised, the decorator wraps the output of the function in anOk.fromtypingimportCallablefromexplicitorimportunraisedeffunction(x:int)->int:ifx<10:raiseValueError("too small")returnx+1safe_func:Callable[[int],Result[int,ValueError]]=unraise(function)What does not workIn Rust, there is the handy?operator, which allows you to quickly return anErrfrom a function or continue if the result is anOk.
In Python, the equivalent might be:result:Result=function()ifresult.is_err():returnresult...There are also a lot of additional methods available on Rust'sOkandErrwhich are thus far not implemented, particularly their translations toOption.Personal experimentation has shown that mypy may not always deal with these types or thematchexpression as expected, certainly I would not consider them as safe as the Rust equivalents.
If you know how to improve the implementation so that it works better with mypy, and/or makes the behavior more Rust-like, feel free to make a PR.ContributingFork and clone the repoCreate a virtual environmentpoetry install