Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

Do Notation

@do binds values from one family inside a generator. Each yield unwraps a success, and the first Err, Nothing or Failure ends the block and becomes its result.

from pyrula import Either, do, Ok, Err
def parse_int(raw: str) -> Either[int, str]:
try:
return Ok(int(raw))
except ValueError:
return Err(f"not an int: {raw!r}")
@do
def total(a: str, b: str):
x = yield parse_int(a)
y = yield parse_int(b)
return x + y
total("2", "3") # Ok(5)
total("nope", "3") # Err("not an int: 'nope'")

Option and Try work the same way:

from pyrula import Option, Nothing
users = {"ada": {"email": "ada@example.com"}}
@do
def lookup(uid: str):
user = yield Option.of(users.get(uid))
email = yield Option.of(user.get("email"))
return email
assert lookup("ada").get_or_else("") == "ada@example.com"
assert lookup("missing") is Nothing
  • A block yields one family. Mixing families raises TypeError.
  • Validated is not supported; use zip, sequence or traverse to accumulate errors.
  • @do does not capture exceptions raised in the body, including in a Try block. Wrap them with Try.apply first.

@do_async drives an async generator the same way. Python forbids return value in an async generator, so the result is the last yielded value:

import asyncio
from pyrula import Either, Ok, Err, do_async
async def fetch_name(uid: str) -> Either[str, str]:
return Ok("Ada") if uid == "ada" else Err("unknown user")
@do_async
async def greeting(uid: str):
name = yield await fetch_name(uid)
yield Ok(f"Hello, {name}")
assert asyncio.run(greeting("ada")) == Ok("Hello, Ada")
assert asyncio.run(greeting("missing")) == Err("unknown user")

In a Try block, @do_async captures Exception from the body or a yielded awaitable as Failure.

View this page as Markdown