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}")
@dodef 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 runsCollect every error at once
Section titled “Collect every error at once”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'])Read config without None checks
Section titled “Read config without None checks”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)Pull an exception into a value
Section titled “Pull an exception into a value”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 inputA result group that can never be empty
Section titled “A result group that can never be empty”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([]) # NothingNonEmptyList.from_list([1, 2]) # Some(NonEmptyList(1, 2))Dedup and membership
Section titled “Dedup and membership”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 droppedseen.contains("b") # Trueseen.union(ISet(["c", "d"])) # new ISet of size 4; `seen` unchangedSplit a batch of results
Section titled “Split a batch of results”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'"])Timeouts as values
Section titled “Timeouts as values”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() # 5000deadline.to_timedelta() # datetime.timedelta(seconds=5)