{"id":16319723,"url":"https://github.com/edeckers/pyella","last_synced_at":"2026-03-12T15:37:32.727Z","repository":{"id":61490608,"uuid":"551871778","full_name":"edeckers/pyella","owner":"edeckers","description":"Common applicatives, functors and monads for loads of /fun/ with Python","archived":false,"fork":false,"pushed_at":"2025-06-10T08:33:32.000Z","size":313,"stargazers_count":5,"open_issues_count":6,"forks_count":3,"subscribers_count":1,"default_branch":"develop","last_synced_at":"2025-10-27T06:49:05.183Z","etag":null,"topics":["applicative","either","fp","functional-programming","functor","hacktoberfest","haskell","just","lambda","maybe","monad","optional","python"],"latest_commit_sha":null,"homepage":"https://pyella.readthedocs.io","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/edeckers.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","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":"2022-10-15T09:25:00.000Z","updated_at":"2025-09-16T20:35:13.000Z","dependencies_parsed_at":"2023-02-06T19:32:02.133Z","dependency_job_id":"19b6abc1-858c-4004-872c-ce7d0e84a428","html_url":"https://github.com/edeckers/pyella","commit_stats":{"total_commits":45,"total_committers":4,"mean_commits":11.25,"dds":0.4222222222222223,"last_synced_commit":"4edbdc1737904b9ef3b5742bc7be6c99b1090aeb"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/edeckers/pyella","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edeckers%2Fpyella","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edeckers%2Fpyella/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edeckers%2Fpyella/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edeckers%2Fpyella/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/edeckers","download_url":"https://codeload.github.com/edeckers/pyella/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edeckers%2Fpyella/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30430949,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-12T14:34:45.044Z","status":"ssl_error","status_checked_at":"2026-03-12T14:09:33.793Z","response_time":114,"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":["applicative","either","fp","functional-programming","functor","hacktoberfest","haskell","just","lambda","maybe","monad","optional","python"],"created_at":"2024-10-10T22:28:02.526Z","updated_at":"2026-03-12T15:37:32.706Z","avatar_url":"https://github.com/edeckers.png","language":"Python","readme":"# Pyella\n\n[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)\n[![Build](https://github.com/edeckers/pyella/actions/workflows/test.yml/badge.svg?branch=develop)](https://github.com/edeckers/pyella/actions/workflows/test.yml)\n[![PyPI](https://img.shields.io/pypi/v/pyella.svg?maxAge=3600)](https://pypi.org/project/pyella)\n[![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)\n\nThe Pyella library brings common monads such as [Maybe](https://hackage.haskell.org/package/base/docs/Data-Maybe.html) and [Either](https://hackage.haskell.org/package/base/docs/Data-Either.html) to your Python projects.\n\nThese monad implementations are strongly inspired by their Haskell namesakes.\n\n## Requirements\n\n- Python 3.8+\n\n## Installation\n\n```bash\npip3 install pyella\n```\n\n## Rationale\n\nSome of the main reasons for writing Pyella were:\n- I prefer the more explicit error handling `Eithers` can bring compared to regular Exception handling\n- Whenever one of my applications crashes due to an NPE, which are almost always avoidable, I die a little inside. `Maybe` can help with that\n- A nice chain of `fmaps`, `binds`, et al looks satisfying to me\n\nBy 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. It _does_ however make you think more about what states your application can end up in and how you want to handle them, in my experience.\n\nAlso consider the cons though, some of which are outlined pretty nicely in this blogpost that deems _Result types_ such as `Either` [leaky abstractions](https://eiriktsarpalis.wordpress.com/2017/02/19/youre-better-off-using-exceptions/). A very valid point! No reason to ditch them altogether, but defintely a warning to use them wisely.\n\n## The name\n\nIt's a nod to Python and sounds like [paella](https://en.wikipedia.org/wiki/Paella) ...that's the intention anyway. Seemed somewhat funny to me at the time and that's pretty much all there is to it.\n\nThere's no connection to the actual purpose of the library :)\n\n## Usage\n\n### Maybe\n\nThe Maybe type represents values that might or might not have a value, and can in many ways be considered the exact same thing as an `Optional`. It brings some extra functionality though, such as mapping, binding and chaining.\n\nA real-world like example for applying `Maybe` would be reading settings from a config file\n\n```python\nfrom pyella.maybe import Maybe\n\n# Let's say we have a trivial configuration loader function\ndef load_config(path_to_config:Path) -\u003e Dict[str, Any]:\n    with open(path_to_config, \"r\") as config_handle:\n       return json.load(config_handle)\n\n# And assume the config file content looks like this\n# {\n#   \"url\": \"https://api.github.com/repos/edeckers/pyella\"\n# }\n\n# We'll build upon this `config_json` variable in the\n# examples below\nconfig_json = load_config(\"/path/to/your.conf.json\")\n```\n\nSay you want to read the `url` setting\n\n```python\nmaybe_url = Maybe.of(config_json.get(\"url\"))\nprint (maybe_url)\n\n# Output:\n#\n# Just(\"https://api.github.com/repos/edeckers/pyella\")\n```\n\nAnd next the 'api_key'\n\n```python\nmaybe_api_key = Maybe.of(config_json.get(\"api_key\"))\nprint (maybe_api_key)\n\n# Output:\n#\n# Nothing\n```\n\nMaybe (!) you want to fallback to another value when the `api_key` is missing from the configuration, let's say to an environment variable\n\n```python\napi_key = maybe_api_key.from_maybe(os.getenv(\"MY_API_KEY\"))\nprint (api_key)\n\n# Output:\n#\n# \u003ca string representing the api key\u003e\n```\n\nAnd for some more trivial examples\n\n```python\nj0 = Maybe.of(1)\nprint (j0)\n# Output: Just(1)\n\nprint (j0.from_maybe(-1))\n# Output: 1\n\nj1 = j0.fmap(lambda x:x*2)\nprint(j0)\nprint(j1)\n# Output:\n#\n# Just(1)\n# Just(2)\n```\n\n### Either\n\nEithers represent values with two possibilities, often an error and success state. It's convention to use `Left` for the error state and `Right` for the success - or the _right_ - state.\n\nA real-world like example for applying `Either` would be the retrieval of a url, something that might _either_ fail or succeed.\n\n```python\nfrom pyella.either import Either, left, lefts, right, rights\n\n# Let's define a very trivial url retriever which returns a\n# `Left\u003cint\u003e` or `Right\u003cResponse\u003e` depending on the status code\ndef fetch_some_interesting_url() -\u003e Either[int, Response]:\n    response = request.get(\"https://api.github.com/repos/edeckers/pyella\")\n\n    return Either.pure(response) \\\n        if response.status_code == 200 else \\\n            left(response.status_code)\n\n# We'll build upon this `error_or_response` variable in the\n# examples below\nerror_or_response = fetch_some_interesting_url()\n```\n\nMaybe you want to print the status code\n\n```python\nstatus_code = error_or_response.if_right(200)\nprint (\"Status code\", status_code)\n\n# Output:\n#\n# Status code \u003cstatus code\u003e\n```\n\nOr maybe you're looking for the occurence of a particular string in the response\n\n```python\nis_my_string_there = \\\n    error_or_response \\\n        .fmap(lambda response:\"monad\" in response.text) \\\n        .if_left(False)\nprint(\"Is my string there?\", is_my_string_there)\n\n# Output:\n#\n# Left: Is my string there? False\n# Right: Is my string there? \u003cTrue or False depending on occurrence\u003e\n```\n\nHow about parsing a succcesful response?\n\n```python\n# Say we define this trivial response parser\ndef parse_response(response: Response) -\u003e Either[int, dict]:\n    try:\n        return Either.of(str(response.json()))\n    except:\n        return left(-1)\n    \nerror_or_parsed_response =\n    error_or_response \\\n        .fmap(parse_response)\nprint (error_or_parsed_response)\n```\n\nSeems like not such a bad idea at first glance, and it works:\n\n```python\n# Output\n#\n# Left: Right(Left(-1))\n# Right: Right(Right({ \"name\": \"pyella\" }))\n```\n\n...but that nesting is a little confusing, unnecessary and reminds me of the\n\n                        P Y R A M I D  O F  D O O M\n\n      https://en.wikipedia.org/wiki/Pyramid_of_doom_(programming)\n\n\nSurely there must be a better way to go about this, and there is!\n\n```python\nerror_or_parsed_response_with_bind =\n    error_or_response.bind(parse_response)\nprint (error_or_parsed_response_with_bind)\n   \n# Output\n#\n# Left: Left(-1)\n# Right: Right({ \"name\": \"pyella\" })\n```\n\nThat's better! :)\n\nOf course, the value in `Left` has a different meaning now - it's no longer a status code - which is _less than ideal_. A way to deal with this is to introduce a common `Error` type. \n\n```python\nclass Error:\n    message: str\n\nclass HttpReponseError(Error):\n    status: int\n\nclass ParseError(Error):\n    code: int\n\ndef fetch_some_interesting_url() -\u003e Either[HttpResponseError, Response]:\n    # Use the same implementation as before, but instead of returning\n    # Left(\u003cint\u003e) wrap the \u003cint\u003e in a `HttpResponseError` first\n\ndef parse_response(response: Response) -\u003e Either[ParseError, dict]:\n    # Use the same implementation as before, but instead of returning\n    # Left(\u003cint\u003e) wrap the \u003cint\u003e in a `ParseError` first\n\n# Output\n#\n# Left: Left(HttpResponseError(\u003cstatus code\u003e) | ParseError(\u003cparse error code\u003e))\n# Right: Right({ \"name\": \"pyella\" })\n```\n\nBut at the end of the day you might not even care about the errors. Pyella's got you covered\n\n```python\nmaybe_response = error_or_parsed_response.to_optional()\nprint (maybe_response)\n\n# Output\n#\n# Left: None\n# Right: { \"name\": \"pyella\" }\n```\n\nOr if you prefer to stay within the Pyella domain\n\n```python\nmaybe_response = error_or_parsed_response.to_maybe()\nprint (maybe_response)\n\n# Output\n#\n# Left: Nothing\n# Right: Just({ \"name\": \"pyella\" })\n```\n\nAnd these are some other, trivial use cases of `Either`\n\n```python\nfrom pyella.either import Either, left, lefts, right, rights\n\ne0: Either[str, int] = left(\"invalid value\")\nprint(e0)\n# Output: Left(invalid value)\n\nprint (e0.if_left(-1))\nprint (e0.if_right(\"the value was valid\"))\n# Output:\n#\n# -1\n# 'invalid value'\n\ne1: Either[str, int] = right(1)\nprint (e1)\n# Output: Right(1)\n\ne2 = e1.fmap(lambda x:x*2)\nprint(e1)\nprint(e2)\n# Output:\n#\n# Right(1)\n# Right(2)\n\nvalid_values = rights([e0, e1, e2])\nprint (valid_values)\n# Output: [1, 2]\n\nchained_result = e1.chain(e2)\n# Output:\n#\n# Right(2)\n\nstring_result = \\\n    e1.either( \\\n        lambda e:f\"FAIL: {e}\", \\\n        lambda v:f\"SUCCESS: {v}\")\n\n# Output:\n#\n# SUCCESS: 1\n```\n\n## Contributing\n\nSee the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the  repository and the development workflow.\n\n## Code of Conduct\n\n[Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.\n\n## License\n\nMPL-2.0\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedeckers%2Fpyella","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedeckers%2Fpyella","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedeckers%2Fpyella/lists"}