# Python The facade is `arxo` at version 0.2.0 with the canon package `arxo-canon-bgb-fristen` at 0.1.1, which carries the canon `de.bgb.fristen` at 0.1.0. You need Python 3.12 or newer. Execution is local through the bundled WebAssembly engine; this release has no serve mode, no registry fetch, and no remote execution, so the only call that reaches a host is `explain` — and a package opened offline refuses even that. ## Install Two routes, different promises: ```bash # Reproduce the pinned example: the versions in the compatibility record. pip install arxo==0.2.0 arxo-canon-bgb-fristen==0.1.1 ``` ```bash # Install current versions: newest compatible pair, not the pinned one. pip install arxo arxo-canon-bgb-fristen ``` The first route installs the pair listed on the compatibility page. The second may install newer releases; re-run the probe on the compatibility page when a version changes. ## A first call The frist scenario from the section map, checked with `truth`: ```python from arxo import open fristen = open("de.bgb.fristen@0.1.0") r = fristen.truth("frist_ende", ["frist", "2026-03-20"], { "legalTime": "2026-09-17", "timezone": "Europe/Berlin", "deadlinePolicy": "urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG", "answers": [ {"predicate": "frist_ereignis", "args": ["frist", "2026-03-06"]}, {"predicate": "frist_dauer_tage", "args": ["frist", "14 calendar_day"]}, ], }) r.evaluationStatus # "COMPUTED" r.truthStatus # "TRUE_ONLY" r.sources # [] on this canon build r.hashes # {"program": ..., "semantic": ..., "result": ...} ``` Remove `frist_dauer_tage` and the answer is `COMPUTED` with `NEITHER`; `why_not` over the partial case returns two blockers, one per candidate rule, each with the premises it was waiting for. The rest of the page is the detail behind this call: [`open`](#open) and how it resolves a spec, the [`LawPackage`](#lawpackage) methods, the [`Answer`](#answers) fields, the [errors](#errors), and the operations Python [does not ship](#what-python-does-not-ship). ## Open ```python def open(spec: str, options: dict[str, Any] | None = None) -> LawPackage: ``` ```python from arxo import open fristen = open("de.bgb.fristen@0.1.0") ``` The spec always carries a version; a versionless spec is refused before anything is resolved: ```python def parse_spec(spec: str | None) -> dict[str, str]: ``` ```python parse_spec("de.bgb.fristen@0.1.0") # {"name": "de.bgb.fristen", "version": "0.1.0"} parse_spec("de.bgb.fristen") # PackageNotFoundError: open() requires name@version ``` Resolution order is an installed canon package, then a checkout tree passed as `local`, then the disk cache. When none of the three sources has the package, `open` raises `PackageNotFoundError` ("registry fetch is not in this release"). The loading chapter details each source and the hash check. There is no serve mode in this release: `open` always returns a `LawPackage` with a locally loaded engine. (TypeScript has serve and router paths that send questions over the network; Python does not ship them.) ## LawPackage Methods are wrappers over `ask`; they do not evaluate twice. The `snake_case` names are canonical; `whyNot` is kept as an alias for the JavaScript name. ```python class LawPackage: def passport(self) -> Passport: ... def explain(self, answer: Any, node_id: str | None = None) -> dict[str, Any]: ... def ask(self, request: dict[str, Any]) -> Answer: ... def truth(self, predicate: str, args: list[Any], case_input: dict[str, Any]) -> Answer: ... def why_not(self, predicate: str, args: list[Any], case_input: dict[str, Any]) -> Answer: ... def whyNot(self, predicate: str, args: list[Any], case_input: dict[str, Any]) -> Answer: ... def collect(self, predicate: str, args: Any, case_input: dict[str, Any]) -> Answer: ... def deadline(self, op_args: dict[str, Any], case_input: dict[str, Any]) -> Answer: ... def calc(self, term: Any, case_input: dict[str, Any]) -> Answer: ... def positions(self, case_input: dict[str, Any]) -> Answer: ... ``` `ask` takes a request dict with `kind` and `caseInput`: ```python fristen.ask({ "kind": "truth", "predicate": "frist_ende", "args": ["frist", "2026-03-20"], "caseInput": case_input, }) ``` Supported kinds are `truth`, `why_not`, `collect`, `calendar_op`, `term`, and `positions`; anything else raises `TypeError`. `collect` names the free position with `?name` in the argument list, or takes an explicit `free` (and optionally a `comprehension`) when `args` is a dict. `deadline` sends `{"op": "deadline", **op_args}` as a `calendar_op`; `calc` sends the term as a `term`. ## Answers ```python @dataclass class Answer: evaluationStatus: str sources: Any proof: Any hashes: dict[str, str] issues: Any document: bytes via: Literal["local", "mcp"] truthStatus: Any = None value: Any = None # Blocker graph of a `why_not` answer; None on other kinds. whyNot: Any = None whyNotTermErrors: Any = None missingInputs: Any = None positions: Any = None ``` `document` is the canonical bytes of the evaluation document — what you archive or hand to a reviewer. `hashes` carries `program`, `semantic`, and `result`. The blocker graph of a `why_not` answer is read from the value map into `whyNot`; on other kinds it is `None`. ## Passport and explain ```python fristen.passport().in_model("frist_ende") # True fristen.passport().inModel("frist_ende") # True, JavaScript name ``` `in_model` says whether the relation exists in the shipped world at all — "not in the model" is a different answer from "the law is silent". `explain` calls the host's `law_explain` over the document the answer carries and returns its text with `via: "mcp"`. It needs the host: an offline package raises `TransportError`, and an answer without a proof graph is refused. ## Errors ```python class LawClientError(Exception): ... # common ancestor of transport failures class IntegrityError(LawClientError): ... class ProtocolError(LawClientError): ... class RpcError(LawClientError): ... class TransportError(LawClientError): ... class ValidationError(LawClientError): ... class FactError(Exception): ... # path, nearest class PackageNotFoundError(Exception): ... # package_name, version ``` Four different situations, three different treatments: - **Absent fact**: a well-formed fact simply not sent. No exception: the answer is a value, `COMPUTED` with `NEITHER` (removing `frist_dauer_tage` from the first call shows it). - **Malformed fact**: wrong shape or arity (`frist_ereignis: expected 2 argument(s), got 0`), an object without kind, an unsupported value, a non-object case, a missing `legalTime`. Raises `FactError` with the failing path. - **Unknown relation**: a predicate the canon never declared (`unknown relation "no_such_relation"`). Raises `FactError`. - **Missing package**: no source has the pinned canon. Raises `PackageNotFoundError` naming the package and version. A fifth case belongs to the application, not the SDK: a required form field the user left empty. The example refuses that with its own validation error before any fact is built — the engine never sees it. The results chapter tables every class with the condition that raises it. ## What Python does not ship Python has no `questions`, no `unfold`, no `focused_truth`, no `verbalize`, no serve mode, no router, and no registry fetch. The workaround for the missing manifest is to read the inventory at author time from the TypeScript side — `questions()` on the same pinned canon returns the facts, questions, parameters, and labels a form or a picker is built from — or to read the canon's shipped index directly. Author-time means the manifest never travels with the Python answer; it is read once while the form is written, and the running code sends only facts the manifest already described. ## Execution and network notes - The evaluator is the bundled `law_wasm_core.wasm`, hosted through `wasmtime`; pass `wasmPath` in the options to load another build. - The calendar comes from the installed canon when it ships one, else from the `calendar` option. - A case already lowered (assertions with `kind: 'assertion'`) is passed through; otherwise the facade assembles the case from `answers` and wraps each bare value by its declared type. - The only network call in this release is `explain` through the MCP client made on first use — unless the package was opened offline, which raises `TransportError` instead. Queries (`truth`, `why_not`, `collect`, `deadline`, `calc`, `positions`) never leave the process: there is no serve path to send them through. ## Where next Back to the [SDK map](/build/sdk/). For an application, read on in this order: [loading and pinning](/build/sdk/loading-and-pinning/) for reproducible versions, [facts and context](/build/sdk/facts-and-context/) for the call shape, and [results and errors](/build/sdk/results-and-errors/) for the answer shape.