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.

Either

Either[T, E] is Ok(value) or Err(error), success type first. There are no Left or Right aliases.

from pyrula import Either, Ok, Err
def parse_port(raw: str) -> Either[int, str]:
if not raw.isascii() or not raw.isdigit():
return Either.err(f"not a number: {raw!r}")
port = int(raw)
return Either.ok(port) if 0 < port < 65536 else Either.err(f"out of range: {port}")

map runs on Ok and skips Err. flat_map takes a function that returns an Either.

def check_available(port: int) -> Either[int, str]:
return Err("port occupied") if port == 8081 else Ok(port)
parse_port("8080").map(lambda p: p + 1) # Ok(8081)
parse_port("nope").map(lambda p: p + 1) # Err('not a number: ...')
parse_port("8080").flat_map(check_available) # Ok(8080)
parse_port("nope").map_err(str.upper) # Err('NOT A NUMBER: ...')
parse_port("nope").recover(lambda err: 8080) # Ok(8080)
parse_port("nope").recover_with(lambda err: parse_port("8080")) # fallback that can fail
parse_port("nope").or_else(lambda: parse_port("8080")) # ignores the error
Ok(2).bimap(str.upper, lambda n: n * 10) # Ok(20); error function first
Ok(Ok(2)).flatten() # Ok(2)

flatten raises TypeError if the Ok does not hold an Either.

ensure(predicate, error) turns a rejected Ok into an Err. A callable error receives the value:

Ok(70000).ensure(lambda p: 0 < p < 65536, lambda p: f"out of range: {p}")
# Err('out of range: 70000')

tap and tap_err observe a case without changing it. Callback exceptions propagate.

parse_port("nope").get_or_else(0) # 0
parse_port("99999").fold(
lambda err: f"rejected: {err}",
lambda port: f"using {port}",
) # "rejected: out of range: 99999"

unwrap_or_raise(ValueError) raises on Err.

get_or_else evaluates its argument eagerly. get_or_else_with(factory) calls the factory once, only on Err.

match parse_port("8080"):
case Ok(port):
message = f"using {port}"
case Err(error):
message = f"rejected: {error}"
assert message == "using 8080"

traverse(values, fn) calls fn in input order and stops at the first Err. sequence(values) collects existing Either values. Both accept generators and return a list inside Ok.

Either.traverse(["8080", "9000"], parse_port) # Ok([8080, 9000])
Either.traverse(["8080", "nope", "9000"], parse_port) # Err("not a number: 'nope'")
Either.sequence([Ok(8080), Ok(9000)]) # Ok([8080, 9000])

A callback that returns a non-Either raises TypeError; callback exceptions propagate. To collect every error, use Validated.traverse.

CallResult
Either.ok(v) / Either.err(e)Ok / Err
Either.try_of(fn)Ok(fn()), or Err if fn raises an Exception
Either.cond(test, ok, err)Ok(ok) if test else Err(err)

to_option() drops the error and returns an Option. @do chains dependent Either calls. The JSON form is in JSON form.

View this page as Markdown