CLI Application
A CLI that takes a user ID, looks them up, and prints a one-line summary. Demonstrates Result[T, E] for parsing errors, sealed unions for typed failure modes, and sys.exit for exit-code propagation.
With argparse
import argparseimport sys
type LookupError = NotFound | BadInput
class NotFound: id: intclass BadInput: detail: str
def parse_id(raw: str) -> Result[int, BadInput]: if not raw.isdigit(): return Err(BadInput(detail=f"not a number: {raw}")) return Ok(int(raw))
def lookup(id: int) -> Result[str, LookupError]: if id == 0: return Err(NotFound(id=id)) return Ok(f"user-{id}")
def main() -> int: let parser = argparse.ArgumentParser() parser.add_argument("user_id", help="numeric user ID") let args = parser.parse_args()
match parse_id(args.user_id): case Err(BadInput(detail)): print(f"error: {detail}", file=sys.stderr) return 2
case Ok(id): match lookup(id): case Ok(name): print(name) return 0 case Err(NotFound(id)): print(f"missing: user {id}", file=sys.stderr) return 4 return 1
if __name__ == "__main__": sys.exit(main())from __future__ import annotationsfrom typhon_runtime import Ok, Err, Resultimport dataclassesimport argparseimport sys
type LookupError = NotFound | BadInput
@dataclasses.dataclass(slots=True)class NotFound: id: int
@dataclasses.dataclass(slots=True)class BadInput: detail: str
def parse_id(raw: str) -> Result[int, BadInput]: if not raw.isdigit(): return Err(BadInput(detail=f"not a number: {raw}")) return Ok(int(raw))
def lookup(id: int) -> Result[str, LookupError]: if id == 0: return Err(NotFound(id=id)) return Ok(f"user-{id}")
def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("user_id", help="numeric user ID") args = parser.parse_args() match parse_id(args.user_id): case Err(BadInput(detail)): print(f"error: {detail}", file=sys.stderr) return 2 case Ok(id): match lookup(id): case Ok(name): print(name) return 0 case Err(NotFound(id)): print(f"missing: user {id}", file=sys.stderr) return 4 return 1
if __name__ == "__main__": sys.exit(main())With click
import sysimport click
# ... LookupError, NotFound, BadInput, parse_id, lookup as above ...
@click.command()@click.argument("user_id")def cli(user_id: str) -> None: match parse_id(user_id): case Err(BadInput(detail)): click.echo(f"error: {detail}", err=True) sys.exit(2)
case Ok(id): match lookup(id): case Ok(name): click.echo(name) case Err(NotFound(id)): click.echo(f"missing: user {id}", err=True) sys.exit(4)
if __name__ == "__main__": cli()Things to notice
- Typed errors with sealed unions.
LookupError = NotFound | BadInput— exhaustive match in the entry point. - Result chaining.
parse_idreturnsResult[int, BadInput];lookupreturnsResult[str, LookupError]. Both compose withmatch. - Exit codes per failure variant.
BadInput→ 2;NotFound→ 4. The CLI’s contract is visible in the source. sys.exit(main())shape. Standard Python idiom. The exit code propagates throughtyc run.
Running
tyc buildpython build/main.py 42# user-42
python build/main.py abc# error: not a number: abc
python build/main.py 0# missing: user 0Or with tyc run:
tyc run -- 42tyc run -- abctyc run -- 0Where next
- Error Handling (tour) —
Resultpatterns. - Sealed Unions and Match (tour) — closed sums.
tyc run— build + exec.