Package: pyella
PyellaThe Pyella library brings common monads such asMaybeandEitherto your Python projects.These monad implementations are strongly inspired by their Haskell namesakes.RequirementsPython 3.8+Installationpip3installpyellaRationaleSome of the main reasons for writing Pyella were:I prefer the more explicit error handlingEitherscan bring compared to regular Exception handlingWhenever one of my applications crashes due to an NPE, which are almost always avoidable, I die a little inside.Maybecan help with thatA nice chain offmaps,binds, et al looks satisfying to meBy no means am I claiming that this library will prevent all NPEs nor that it will prevent any other errors being thrown, because that's not how Python works. Itdoeshowever make you think more about what states your application can end up in and how you want to handle them, in my experience.Also consider the cons though, some of which are outlined pretty nicely in this blogpost that deemsResult typessuch asEitherleaky abstractions. A very valid point! No reason to ditch them altogether, but defintely a warning to use them wisely.The nameIt's a nod to Python and sounds likepaella...that's the intention anyway. Seemed somewhat funny to me at the time and that's pretty much all there is to it.There's no connection to the actual purpose of the library :)UsageMaybeThe Maybe type represents values that might or might not have a value, and can in many ways be considered the exact same thing as anOptional. It brings some extra functionality though, such as mapping, binding and chaining.A real-world like example for applyingMaybewould be reading settings from a config filefrompyella.maybeimportMaybe# Let's say we have a trivial configuration loader functiondefload_config(path_to_config:Path)->Dict[str,Any]:withopen(path_to_config,"r")asconfig_handle:returnjson.load(config_handle)# And assume the config file content looks like this# {#   "url": "https://api.github.com/repos/edeckers/pyella"# }# We'll build upon this `config_json` variable in the# examples belowconfig_json=load_config("/path/to/your.conf.json")Say you want to read theurlsettingmaybe_url=Maybe.of(config_json.get("url"))print(maybe_url)# Output:## Just("https://api.github.com/repos/edeckers/pyella")And next the 'api_key'maybe_api_key=Maybe.of(config_json.get("api_key"))print(maybe_api_key)# Output:## NothingMaybe (!) you want to fallback to another value when theapi_keyis missing from the configuration, let's say to an environment variableapi_key=maybe_api_key.from_maybe(os.getenv("MY_API_KEY"))print(api_key)# Output:## <a string representing the api key>And for some more trivial examplesj0=Maybe.of(1)print(j0)# Output: Just(1)print(j0.from_maybe(-1))# Output: 1j1=j0.fmap(lambdax:x*2)print(j0)print(j1)# Output:## Just(1)# Just(2)EitherEithers represent values with two possibilities, often an error and success state. It's convention to useLeftfor the error state andRightfor the success - or theright- state.A real-world like example for applyingEitherwould be the retrieval of a url, something that mighteitherfail or succeed.frompyella.eitherimportEither,left,lefts,right,rights# Let's define a very trivial url retriever which returns a# `Left<int>` or `Right<Response>` depending on the status codedeffetch_some_interesting_url()->Either[int,Response]:response=request.get("https://api.github.com/repos/edeckers/pyella")returnEither.pure(response)\ifresponse.status_code==200else\left(response.status_code)# We'll build upon this `error_or_response` variable in the# examples belowerror_or_response=fetch_some_interesting_url()Maybe you want to print the status codestatus_code=error_or_response.if_right(200)print("Status code",status_code)# Output:## Status code <status code>Or maybe you're looking for the occurence of a particular string in the responseis_my_string_there=\error_or_response\.fmap(lambdaresponse:"monad"inresponse.text)\.if_left(False)print("Is my string there?",is_my_string_there)# Output:## Left: Is my string there? False# Right: Is my string there? <True or False depending on occurrence>How about parsing a succcesful response?# Say we define this trivial response parserdefparse_response(response:Response)->Either[int,dict]:try:returnEither.of(str(response.json()))except:returnleft(-1)error_or_parsed_response=error_or_response\.fmap(parse_response)print(error_or_parsed_response)Seems like not such a bad idea at first glance, and it works:# Output## Left: Right(Left(-1))# Right: Right(Right({ "name": "pyella" }))...but that nesting is a little confusing, unnecessary and reminds me of theP Y R A M I D  O F  D O O M

  https://en.wikipedia.org/wiki/Pyramid_of_doom_(programming)Surely there must be a better way to go about this, and there is!error_or_parsed_response_with_bind=error_or_response.bind(parse_response)print(error_or_parsed_response_with_bind)# Output## Left: Left(-1)# Right: Right({ "name": "pyella" })That's better! :)Of course, the value inLefthas a different meaning now - it's no longer a status code - which isless than ideal. A way to deal with this is to introduce a commonErrortype.classError:message:strclassHttpReponseError(Error):status:intclassParseError(Error):code:intdeffetch_some_interesting_url()->Either[HttpResponseError,Response]:# Use the same implementation as before, but instead of returning# Left(<int>) wrap the <int> in a `HttpResponseError` firstdefparse_response(response:Response)->Either[ParseError,dict]:# Use the same implementation as before, but instead of returning# Left(<int>) wrap the <int> in a `ParseError` first# Output## Left: Left(HttpResponseError(<status code>) | ParseError(<parse error code>))# Right: Right({ "name": "pyella" })But at the end of the day you might not even care about the errors. Pyella's got you coveredmaybe_response=error_or_parsed_response.to_optional()print(maybe_response)# Output## Left: None# Right: { "name": "pyella" }Or if you prefer to stay within the Pyella domainmaybe_response=error_or_parsed_response.to_maybe()print(maybe_response)# Output## Left: Nothing# Right: Just({ "name": "pyella" })And these are some other, trivial use cases ofEitherfrompyella.eitherimportEither,left,lefts,right,rightse0:Either[str,int]=left("invalid value")print(e0)# Output: Left(invalid value)print(e0.if_left(-1))print(e0.if_right("the value was valid"))# Output:## -1# 'invalid value'e1:Either[str,int]=right(1)print(e1)# Output: Right(1)e2=e1.fmap(lambdax:x*2)print(e1)print(e2)# Output:## Right(1)# Right(2)valid_values=rights([e0,e1,e2])print(valid_values)# Output: [1, 2]chained_result=e1.chain(e2)# Output:## Right(2)string_result=\e1.either(\lambdae:f"FAIL:{e}",\lambdav:f"SUCCESS:{v}")# Output:## SUCCESS: 1ContributingSee thecontributing guideto learn how to contribute to the  repository and the development workflow.Code of ConductContributor Code of Conduct. By participating in this project you agree to abide by its terms.LicenseMPL-2.0