Modeling Types
newtype
Section titled “newtype”newtype makes a UserId distinct from a bare int for type checkers, while it behaves like one at runtime.
from pyrula import newtype, newtype_strict
UserId = newtype("UserId", int)UserId(1) # 1, typed as UserId
Email = newtype_strict("Email", str, validate=lambda s: "@" in s)Email("nope") # raises ValueErrornewtype_strict also takes err_msg for the failure message.
Sealed unions
Section titled “Sealed unions”@sealed marks a closed set of cases and @case declares each as an immutable dataclass. match runs the first matching on(Type, handler) and raises if none match.
from pyrula import sealed, case, match, on
@sealedclass Shape: pass
@caseclass Circle(Shape): radius: float
@caseclass Rect(Shape): w: float h: float
def area(shape: Shape) -> float: return match(shape)( on(Circle, lambda c: 3.14159 * c.radius ** 2), on(Rect, lambda r: r.w * r.h), )
area(Circle(2.0)) # 12.566...A sealed type accepts subclasses only from its defining module; any other raises TypeError at class creation, with no opt-out for tests.
Cases are frozen dataclasses, so dataclasses.replace and asdict work:
import dataclasses
c = Circle(2.0)dataclasses.replace(c, radius=3.0) # Circle(radius=3.0)dataclasses.asdict(c) # {"radius": 2.0}The JSON form is {"CaseName": {fields}}; see JSON form.