Skip to content
Pyrula

Examples

Short, practical snippets that mix the primitives the way you would in real code. Each one runs as written.

Parse and validate, stop at the first error

Section titled “Parse and validate, stop at the first error”

Either plus do-notation reads like straight-line code but short-circuits on the first failure.

from pyrula import Ok, Err, do
def parse_int(s: str):
return Ok(int(s)) if s.lstrip("-").isdigit() else Err(f"not an int: {s!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("2", "x") # Err("not an int: 'x'"), second parse never runs

When you want all the problems, not just the first, reach for Validated. zip combines results and accumulates the errors.

from pyrula import Valid, Validated
def check_name(name: str):
return Valid(name) if name else Validated.invalid("name required")
def check_age(age: int):
return Valid(age) if age >= 0 else Validated.invalid("age must be >= 0")
check_name("").zip(check_age(-1))
# Invalid(['name required', 'age must be >= 0'])

Option turns “might be missing” into a chain that handles absence once, at the end.

from pyrula import Option
cfg = {"timeout": "30"}
timeout = (
Option.of(cfg.get("timeout")) # Some("30") or Nothing
.map(int) # Some(30)
.filter(lambda s: s > 0) # still Some(30)
.get_or_else(10) # 30; would be 10 if missing or non-positive
)

Try captures whatever a risky call throws, and to_either hands you a typed error to carry onward.

from pyrula import Try
result = Try.apply(lambda: int(user_input)).to_either()
# Ok(42) on success, Err(ValueError(...)) on bad input

NonEmptyList makes “at least one” part of the type, so reduce needs no empty-case handling.

from pyrula import NonEmptyList
scores = NonEmptyList(5, 3, 8)
scores.reduce(lambda a, b: max(a, b)) # 8, total, no seed value needed
NonEmptyList.from_list([]) # Nothing
NonEmptyList.from_list([1, 2]) # Some(NonEmptyList(1, 2))

ISet is an immutable set with the usual algebra, so building up a set of “seen” keys never mutates a shared value.

from pyrula import ISet
seen = ISet(["a", "b", "a", "c"]) # size 3, duplicates dropped
seen.contains("b") # True
seen.union(ISet(["c", "d"])) # new ISet of size 4; `seen` unchanged

After processing a list of items, partition_either separates what worked from what didn’t in one pass. No manual loop, no mutable accumulators.

from pyrula import IList, Ok, Err
def parse_int(s: str):
return Ok(int(s)) if s.lstrip("-").isdigit() else Err(f"bad: {s!r}")
results = IList(["1", "two", "3", "four"]).map(parse_int)
ok_vals, errors = results.partition_either()
# ok_vals: IList([1, 3])
# errors: IList(["bad: 'two'", "bad: 'four'"])

Duration replaces bare second/millisecond ints with a typed quantity you can do math on.

from pyrula import Duration
poll = Duration.seconds(0.5)
deadline = poll * 10 # Duration(5s)
deadline.to_millis() # 5000
deadline.to_timedelta() # datetime.timedelta(seconds=5)