Package: result-test-publish-version
ResultA simple Result type for Python 3inspired by
Rust, fully type annotated.InstallationLatest release:$pipinstallresultLatest GitHubmasterbranch version:$pipinstallgit+https://github.com/rustedpy/resultSummaryThe idea is that a result value can be eitherOk(value)orErr(error), with a way to differentiate between the two.OkandErrare both classes encapsulating an arbitrary value.Result[T, E]is a generic type alias fortyping.Union[Ok[T], Err[E]]. It will
change code like this:defget_user_by_email(email:str)->Tuple[Optional[User],Optional[str]]:"""Return the user instance or an error message."""ifnotuser_exists(email):returnNone,'User does not exist'ifnotuser_active(email):returnNone,'User is inactive'user=get_user(email)returnuser,Noneuser,reason=get_user_by_email('ueli@example.com')ifuserisNone:raiseRuntimeError('Could not fetch user:%s'%reason)else:do_something(user)To something like this:fromresultimportOk,Err,Result,is_ok,is_errdefget_user_by_email(email:str)->Result[User,str]:"""Return the user instance or an error message."""ifnotuser_exists(email):returnErr('User does not exist')ifnotuser_active(email):returnErr('User is inactive')user=get_user(email)returnOk(user)user_result=get_user_by_email(email)ifisinstance(user_result,Ok):# or `is_ok(user_result)`# type(user_result.ok_value) == Userdo_something(user_result.ok_value)else:# or `elif is_err(user_result)`# type(user_result.err_value) == strraiseRuntimeError('Could not fetch user:%s'%user_result.err_value)Note that.ok_valueexists only on an instance ofOkand.err_valueexists only on an instance ofErr.And if you're using python version3.10or later, you can use the
elegantmatchstatement as well:fromresultimportResult,Ok,Errdefdivide(a:int,b:int)->Result[int,str]:ifb==0:returnErr("Cannot divide by zero")returnOk(a//b)values=[(10,0),(10,5)]fora,binvalues:matchdivide(a,b):caseOk(value):print(f"{a}//{b}=={value}")caseErr(e):print(e)Not all methods
(https://doc.rust-lang.org/std/result/enum.Result.html) have been
implemented, only the ones that make sense in the Python context. By
usingisinstanceto check forOkorErryou get type safe access
to the contained value when usingMyPyto typecheck your code. All of this in a package allowing easier
handling of values that can be OK or not, without resorting to custom
exceptions.APIAuto generated API docs are also available at./docs/README.md.Creating an instance:>>>fromresultimportOk,Err>>>res1=Ok('yay')>>>res2=Err('nay')Checking whether a result isOkorErr. You can either useis_okandis_errtype guardfunctionsorisinstance. This way you get
type safe access that can be checked with MyPy. Theis_ok()oris_err()methodscan be used if you don't need the type safety
with MyPy:>>>res=Ok('yay')>>>isinstance(res,Ok)True>>>is_ok(res)True>>>isinstance(res,Err)False>>>is_err(res)False>>>res.is_ok()True>>>res.is_err()FalseYou can also check if an object isOkorErrby using theOkErrtype. Please note that this type is designed purely for convenience, and
should not be used for anything else. Using(Ok, Err)also works fine:>>>res1=Ok('yay')>>>res2=Err('nay')>>>isinstance(res1,OkErr)True>>>isinstance(res2,OkErr)True>>>isinstance(1,OkErr)False>>>isinstance(res1,(Ok,Err))TrueConvert aResultto the value orNone:>>>res1=Ok('yay')>>>res2=Err('nay')>>>res1.ok()'yay'>>>res2.ok()NoneConvert aResultto the error orNone:>>>res1=Ok('yay')>>>res2=Err('nay')>>>res1.err()None>>>res2.err()'nay'Access the value directly, without any other checks:>>>res1=Ok('yay')>>>res2=Err('nay')>>>res1.ok_value'yay'>>>res2.err_value'nay'Note that this is a property, you cannot assign to it. Results are
immutable.When the value inside is irrelevant, we suggest usingNoneor abool, but you're free to use any value you think works best. An
instance of aResult(OkorErr) must always contain something. If
you're looking for a type that might contain a value you may be
interested in amaybe.Theunwrapmethod returns the value ifOkandunwrap_errmethod
returns the error value ifErr, otherwise it raises anUnwrapError:>>>res1=Ok('yay')>>>res2=Err('nay')>>>res1.unwrap()'yay'>>>res2.unwrap()Traceback(mostrecentcalllast):File"<stdin>",line1,in<module>File"C:\project\result\result.py",line107,inunwrapreturnself.expect("Called `Result.unwrap()` on an `Err` value")File"C:\project\result\result.py",line101,inexpectraiseUnwrapError(message)result.result.UnwrapError:Called`Result.unwrap()`onan`Err`value>>>res1.unwrap_err()Traceback(mostrecentcalllast):...>>>res2.unwrap_err()'nay'A custom error message can be displayed instead by usingexpectandexpect_err:>>>res1=Ok('yay')>>>res2=Err('nay')>>>res1.expect('not ok')'yay'>>>res2.expect('not ok')Traceback(mostrecentcalllast):File"<stdin>",line1,in<module>File"C:\project\result\result.py",line101,inexpectraiseUnwrapError(message)result.result.UnwrapError:notok>>>res1.expect_err('not err')Traceback(mostrecentcalllast):...>>>res2.expect_err('not err')'nay'A default value can be returned instead by usingunwrap_ororunwrap_or_else:>>>res1=Ok('yay')>>>res2=Err('nay')>>>res1.unwrap_or('default')'yay'>>>res2.unwrap_or('default')'default'>>>res1.unwrap_or_else(str.upper)'yay'>>>res2.unwrap_or_else(str.upper)'NAY'Theunwrapmethod will raised anUnwrapError. A custom exception can
be raised by using theunwrap_or_raisemethod instead:>>>res1=Ok('yay')>>>res2=Err('nay')>>>res1.unwrap_or_raise(ValueError)'yay'>>>res2.unwrap_or_raise(ValueError)ValueError:nayValues and errors can be mapped usingmap,map_or,map_or_elseandmap_err:>>>Ok(1).map(lambdax:x+1)Ok(2)>>>Err('nay').map(lambdax:x+1)Err('nay')>>>Ok(1).map_or(-1,lambdax:x+1)2>>>Err(1).map_or(-1,lambdax:x+1)-1>>>Ok(1).map_or_else(lambda:3,lambdax:x+1)2>>>Err('nay').map_or_else(lambda:3,lambdax:x+1)3>>>Ok(1).map_err(lambdax:x+1)Ok(1)>>>Err(1).map_err(lambdax:x+1)Err(2)To save memory, both theOkandErrclasses are ‘slotted’, i.e. they
define__slots__. This means assigning arbitrary attributes to
instances will raiseAttributeError.as_resultDecoratorTheas_result()decorator can be used to quickly turn ‘normal’
functions intoResultreturning ones by specifying one or more
exception types:@as_result(ValueError,IndexError)deff(value:int)->int:ifvalue==0:raiseValueError# becomes Errelifvalue==1:raiseIndexError# becomes Errelifvalue==2:raiseKeyError# raises Exceptionelse:returnvalue# becomes Okres=f(0)# Err[ValueError()]res=f(1)# Err[IndexError()]res=f(2)# raises KeyErrorres=f(3)# Ok[3]Exception(or evenBaseException) can be specified to create a
‘catch all’Resultreturn type. This is effectively the same astryfollowed byexcept Exception, which is not considered good practice in
most scenarios, and hence this requires explicit opt-in.Sinceas_resultis a regular decorator, it can be used to wrap
existing functions (also from other libraries), albeit with a slightly
unconventional syntax (without the usual@):importthird_partyx=third_party.do_something(...)# could raise; who knows?safe_do_something=as_result(Exception)(third_party.do_something)res=safe_do_something(...)# Ok(...) or Err(...)ifisinstance(res,Ok):print(res.ok_value)Do notationDo notation is syntactic sugar for a sequence ofand_then()calls.
Much like the equivalent in Rust or Haskell, but with different syntax.
Instead ofx <- Ok(1)we writefor x in Ok(1). Since the syntax is
generator-based, the final result must be the first line, not the last.final_result:Result[int,str]=do(Ok(x+y)forxinOk(1)foryinOk(2))Note that if you exclude the type annotation,final_result: Result[float, int] = ..., your type checker may be
unable to infer the return type. To avoid an errors or warnings from
your type checker, you should add a type hint when using thedofunction.This is similar to Rust'sm!
macro:usedo_notation::m;letr=m!{x<-Some(1);y<-Some(2);Some(x+y)};Note that if your do statement has multiplefor`s, you can access an identifier bound in a
previous `for. Example:my_result:Result[int,str]=do(f(x,y,z)forxinget_x()foryincalculate_y_from_x(x)forzincalculate_z_from_x_y(x,y))You can usedo()with awaited values as follows:asyncdefprocess_data(data)->Result[int,str]:res1=awaitget_result_1(data)res2=awaitget_result_2(data)returndo(Ok(x+y)forxinres1foryinres2)However, if you want to await something inside the expression, usedo_async():asyncdefprocess_data(data)->Result[int,str]:returndo_async(Ok(x+y)forxinawaitget_result_1(data)foryinawaitget_result_2(data))Troubleshootingdo()calls:TypeError("Got async_generator but expected generator")Sometimes regulardo()can handle async values, but this error means
you have hit a case where it does not. You should usedo_async()here
instead.ContributingThese steps should work on any Unix-based system (Linux, macOS, etc) with Python
andmakeinstalled. On Windows, you will need to refer to the Python
documentation (linked below) and reference theMakefilefor commands to run
from the non-unix shell you're using on Windows.Setup and activate a virtual environment. SeePython docsfor more
information about virtual environments and setup.Runmake installto install dependenciesSwitch to a new git branch and make your changesTest your changes:make testmake lintYou can also start a Python REPL and importresultUpdate documentationEdit any relevant docstrings, markdown filesRunmake docsAdd an entry to thechangelogGit commit all your changes and create a new PR.FAQWhy do I get the "Cannot infer type argument" error with MyPy?There isa bug in MyPywhich can be triggered in some scenarios. Usingif isinstance(res, Ok)instead ofif res.is_ok()will help in some cases. Otherwise usingone of these
workaroundscan help.LicenseMIT License