Python SDK
The Python SDK is a producer, not a verifier. It builds portable audit events, evidence edges, evidence commits, governed action drafts, risk assessments, and security.risk assertions, and it hashes all of them with the same canonical bytes TypeScript and Go produce. It does not verify record chains, does not record provenance, does not assemble export bundles, and does not own storage.
That boundary is the most important thing on this page. A Python service can participate fully in a Veritio deployment on the write side. On the read-and-prove side, one of the three chain verifiers exists in Python and two do not.
What the package contains
Section titled “What the package contains”The distribution is one module tree with five files, and everything public is re-exported from the package root.
veritio/ event.py events, edges, commits, canonical JSON, hashing, redaction governed_change.py entity definitions, change and action drafts, revision ids risk.py signal normalization, per-step scoring, episode rollup, assertions risk_policy.py temperature-derived policies over DEFAULT_RISK_POLICY templates.py 26 event builders plus the audit-log classification helpersThere is no store.py, no provenance.py, and no export_bundle.py. Their absence is a package boundary, not a protocol gap: the protocol defines those behaviors, and at the verified revision only TypeScript implements them.
Install the verified revision
Section titled “Install the verified revision”The package is veritio version 0.0.1 and is installed from the pinned checkout rather than from an index.
git clone https://github.com/getveritio/veritio.gitgit -C veritio checkout c4100ee7b678d0c6b227c67ae6ea1d8a1f373967python3 -m pip install -e ./veritio/sdks/pythonpyproject.toml declares requires-python = ">=3.11". Confirm which checkout is actually imported:
python3 -c "import veritio; print(veritio.__file__)"The printed path must resolve inside the checked-out veritio/sdks/python/src/veritio directory. If it resolves anywhere else, every hash on this page is being produced by code you have not pinned.
Create the shared event
Section titled “Create the shared event”from veritio import create_audit_event, hash_audit_event
event = create_audit_event( { "id": "evt_member_invited_01", "occurredAt": "2026-08-09T10:00:00.000Z", "actor": {"type": "user", "id": "usr_123"}, "action": "organization.member.invited", "target": {"type": "organization", "id": "org_acme"}, "scope": {"tenantId": "org_acme", "environment": "production"}, "purpose": "access_management", "lawfulBasis": "contract", "retention": "security_1y", "metadata": {"role": "viewer"}, })
event_hash = hash_audit_event(event)print({"eventId": event["id"], "hashPrefix": event_hash[:12]})python3 src/examples/quickstart/python.py{'eventId': 'evt_member_invited_01', 'hashPrefix': '3c1eb99f7a8f'}create_audit_event fails closed before it produces anything. It raises on a missing actor.id, actor.type, action, target.id, or target.type, and on an action that does not match the dotted lowercase protocol form. It sorts and de-duplicates dataCategories, drops None fields entirely, and passes metadata through deterministic key-pattern redaction before the value is ever hashed. Redaction happens on the way in, so a redacted event and its hash are consistent by construction; there is no separate “redact later” step to forget.
What Python has and what it does not
Section titled “What Python has and what it does not”| Capability | Python surface | Status |
|---|---|---|
| Events, edges, canonical JSON, hashing | create_audit_event, create_evidence_edge, canonical_json, hash_audit_event, hash_audit_record, hash_evidence_edge, hash_evidence_edge_record |
present |
| Idempotency digest | hash_idempotency_key |
present |
| Evidence commits | create_evidence_commit, hash_evidence_commit, verify_evidence_commits |
present |
| Governed changes | define_entity, create_governed_change_draft, create_governed_action_draft, governed_revision_id |
present |
| Risk | normalize_risk_signals, score_risk_signals, rollup_episode_risk, risk_policy, band_of, with_risk_signals |
present |
security.risk assertions |
create_security_risk_assertion, hash_assertion_record, build_security_risk_assessed_event |
present |
| Templates | 26 builders plus audit_templates, audit_template_sets |
present |
| Audit-record verifier | verify_audit_records |
absent — TypeScript only |
| Edge-record verifier | verify_evidence_edge_records |
absent — TypeScript only |
| Provenance recorder | create_provenance_recorder |
absent — TypeScript only |
Export bundles (vevb-1) |
build_export_bundle, verify_export_bundle |
absent — TypeScript only |
| Stores and recorders | AuditStore, MemoryAuditStore, create_audit_recorder |
absent — TypeScript only |
The four absent groups are not stubs that raise; the names do not exist. from veritio import verify_audit_records is an ImportError, not a runtime failure later.
The verifier asymmetry
Section titled “The verifier asymmetry”Veritio has three independent chains. Python implements the verifier for exactly one of them.
audit record chain → verify_audit_records absent in Pythonedge record chain → verify_evidence_edge_records absent in Pythoncommit ledger → verify_evidence_commits present in Pythonverify_evidence_commits walks each streamId independently and returns {"ok": True} or {"ok": False, "index": n, "reason": "..."} at the first failure. It checks the hash algorithm, the veritio-json-v1 canonicalization label, the veritio-merkle-v1 tree algorithm, per-stream sequence continuity, previous-hash linkage, member-manifest normalization, recordCount, the recomputed recordsRoot, and the recomputed commit hash.
It deliberately does not reconcile member record hashes against independently verified records. A commit chain built entirely over fabricated record hashes verifies clean in isolation. The module’s own docstring says to compose it with verify_audit_records and verify_evidence_edge_records — functions that do not exist in this package. Treat a Python-side commit pass as a statement about the ledger, and run end-to-end record verification in a TypeScript process or against your own implementation of the rules in Hash chain.
Risk scoring is protocol math, not a TypeScript detail
Section titled “Risk scoring is protocol math, not a TypeScript detail”risk.py is a byte-for-byte port of the TypeScript scorer. Every operation uses only clamp, floor, divide, and multiply — never pow, exp, or log — because those are the operations whose results are guaranteed to agree across language runtimes. Every constant lives in DEFAULT_RISK_POLICY under policyVersion veritio.reference.v1.
The parity fixture scores the same three pinned signal sets the TypeScript fixture scores and prints the score, level, policy version, and ordered factor contributions.
"""Cross-language parity twin of ``src/examples/risk/score-and-explain.ts``.
The invariant this fixture proves: risk scoring is language-neutral protocolmath, not a TypeScript implementation detail. It scores the SAME three pinnedsignal sets as the TypeScript fixture, under the same reference policy(``veritio.reference.v1``), and prints the score, level, policyVersion and theordered factor contributions. Every printed number must match the correspondingvalue in ``src/examples/risk/expected/score-and-explain.txt`` exactly — if thePython port of ``round4`` / ``_sat`` / ``clamp01`` ever drifts from theTypeScript one, a documented score moves here and the two expected outputs stopagreeing.
Two details make the comparison honest rather than cosmetic:
* The signal dicts are copied verbatim from the TypeScript fixture, including the deliberately sparse first case, so the fail-closed normalization defaults (recoverable / production / magnitude 0) are exercised in both languages.* Numbers are rendered through ``canonical_json``, the cross-language byte contract, so a whole-valued float prints as ``1`` the way TS and Go emit it instead of Python's ``1.0``. Comparing raw ``repr()`` would report a false divergence on a score that is in fact byte-identical.
Deterministic by construction: literal inputs, pinned policy constants, noclock, no randomness, no filesystem.
Run with the sibling SDK on the path:
PYTHONPATH=../veritio/sdks/python/src python3 src/examples/risk/parity.py"""
from typing import Any
from veritio import ( DEFAULT_RISK_POLICY, canonical_json, normalize_risk_signals, score_risk_signals,)
# Pinned verbatim from src/examples/risk/score-and-explain.ts. Do not "improve"# these values: their only job is to be the same bytes the TypeScript fixture# scores, so the two expected outputs can be diffed field by field.RISK_SCENARIOS: list[dict[str, Any]] = [ { "name": "read-config-lookup", "signals": {"operationType": "read"}, }, { "name": "bulk-export-staging", "signals": { "operationType": "bulk", "reversibility": "reversible", "envCriticality": "staging", "dataVolume": 5000, "fanOut": 3, "referenceCount": 12, }, }, { "name": "destructive-drop-production", "signals": { "operationType": "destructive", "reversibility": "irreversible", "envCriticality": "production", "dataVolume": 250000, "fanOut": 40, "referenceCount": 180, }, },]
def canonical_number(value: float) -> str: """Render one score/contribution the way canonical JSON hashes it.
Parity is a claim about bytes, so the fixture must not print Python's float repr: ``clamp01`` returns ``1.0`` for a saturated score while TypeScript emits ``1``. ``canonical_json`` already coerces whole-valued finite floats to int for exactly this reason, so routing every number through it makes the printed line comparable to the TypeScript expected output instead of manufacturing a difference that no hash would ever see. """ return canonical_json(value)
def factor_line(assessment: dict[str, Any]) -> str: """Flatten the ordered factor breakdown into one comparable line.
Factor ORDER is part of the cross-language conformance contract, not a presentation choice, so the line is emitted in the order the scorer returns rather than sorted: a reordered breakdown must show up as a diff here. """ return " ".join( f"{factor['key']}:{canonical_number(factor['contribution'])}" for factor in assessment["factors"] )
def main() -> None: """Print the parity report for every pinned scenario under the reference policy.
Kept free of any ambient input (clock, environment, argv) so the program is byte-identical across runs and machines; the documentation pipeline diffs this stdout against expected/parity.txt. """ print(f"policyVersion={DEFAULT_RISK_POLICY['policyVersion']}") print(f"bands={canonical_json(DEFAULT_RISK_POLICY['bands'])}") for scenario in RISK_SCENARIOS: assessment = score_risk_signals(scenario["signals"], DEFAULT_RISK_POLICY) print("") print(f"scenario={scenario['name']}") print(f" signals={canonical_json(scenario['signals'])}") print(f" normalized={canonical_json(normalize_risk_signals(scenario['signals']))}") print(f" score={canonical_number(assessment['score'])}") print(f" level={assessment['level']}") print(f" policyVersion={assessment['policyVersion']}") print(f" factors={factor_line(assessment)}")
if __name__ == "__main__": main()policyVersion=veritio.reference.v1bands={"critical":0.75,"high":0.5,"low":0.05,"medium":0.25}
scenario=read-config-lookup signals={"operationType":"read"} normalized={"dataVolume":0,"envCriticality":"production","fanOut":0,"operationType":"read","referenceCount":0,"reversibility":"recoverable"} score=0.05 level=low policyVersion=veritio.reference.v1 factors=operationType:0.05 dataVolume:0 fanOut:0 referenceCount:0 reversibility:1 envCriticality:1
scenario=bulk-export-staging signals={"dataVolume":5000,"envCriticality":"staging","fanOut":3,"operationType":"bulk","referenceCount":12,"reversibility":"reversible"} normalized={"dataVolume":5000,"envCriticality":"staging","fanOut":3,"operationType":"bulk","referenceCount":12,"reversibility":"reversible"} score=0.3718 level=medium policyVersion=veritio.reference.v1 factors=operationType:0.55 dataVolume:0.1961 fanOut:0.0129 referenceCount:0.0155 reversibility:0.6 envCriticality:0.8
scenario=destructive-drop-production signals={"dataVolume":250000,"envCriticality":"production","fanOut":40,"operationType":"destructive","referenceCount":180,"reversibility":"irreversible"} normalized={"dataVolume":250000,"envCriticality":"production","fanOut":40,"operationType":"destructive","referenceCount":180,"reversibility":"irreversible"} score=1 level=critical policyVersion=veritio.reference.v1 factors=operationType:0.85 dataVolume:0.1999 fanOut:0.0738 referenceCount:0.0626 reversibility:1.3 envCriticality:1Two details make this a real comparison rather than a cosmetic one. The first scenario is deliberately sparse, so the fail-closed normalization defaults — reversibility: recoverable, envCriticality: production, magnitudes 0 — are exercised in both languages instead of being supplied by the fixture. And every number is rendered through canonical_json, so a saturated score prints as 1 the way TypeScript emits it rather than as Python’s 1.0. Comparing raw repr() would report a divergence on a score that is byte-identical under hashing.
Factor order is part of the contract. The line is emitted in the order the scorer returns, not sorted, so a reordered breakdown shows up as a diff.
score_risk_signals(signals, policy=DEFAULT_RISK_POLICY) takes the policy as an optional keyword, matching TypeScript. Go requires it explicitly. Python raises on an unknown operationType, reversibility, or envCriticality, and on a magnitude that is not a non-negative integer — it never silently degrades an unrecognized signal to a low score.
One small surface asymmetry: clamp01 and round4 are public in veritio.risk but are not re-exported from the package root, so from veritio import round4 fails while from veritio.risk import round4 works. _sat is private in both. band_of is re-exported from the root.
Governed action drafts derive identical identifiers
Section titled “Governed action drafts derive identical identifiers”create_governed_action_draft is the host application’s server-side mutation boundary helper. It is present in Python with the same semantics as createGovernedActionDraft and CreateGovernedActionDraft, pinned by spec/conformance/governed-action-draft.json.
"""Cross-language parity twin of ``src/examples/governed/action-draft.ts``.
The invariant this fixture proves: governed-action derivation islanguage-neutral protocol math, not a TypeScript implementation detail. Itdrafts the SAME pinned subscription upgrade as the TypeScript fixture — thesame entity definition, the same before/after rows, the same``occurredAt``/``idempotencyKey`` — and prints the derived identifiers a hostapplication depends on at its mutation boundary:
* ``create_governed_action_draft`` derives the change id and the activity id from ONE seed, sha256(``tenantId``:``idempotencyKey``), so a replayed mutation reproduces the same change/activity pair in Python that it does in TypeScript and Go instead of forking the graph per SDK.* ``define_entity`` is the only place field capture is decided: the ``omit`` field never reaches the state commitment (and so can never become a governed changed path), and the ``content_digest`` field is committed as a digest rather than a raw value.* The state-commitment digest and the derived ``revisionId`` are byte-identical to the TypeScript ones, which is the real parity claim: canonical JSON, sha256 framing and field ordering agree across languages.* The draft is INPUT ONLY. It carries no store-assigned sequence and no record hash; nothing is evidence until a conforming ``AuditStore`` appends it.
The printed JSON deliberately mirrors the TypeScript fixture's structure keyfor key, so ``src/examples/governed/expected/action-draft.txt`` and``src/examples/governed/expected/action-draft-python.txt`` can be diffeddirectly. Exactly one line is allowed to differ: the prose ``note``, whichnames the language's own entrypoint (``create_governed_action_draft`` vs``createGovernedActionDraft``). Any diff on an id, digest, changed path, eventaction or edge relation is a real cross-language divergence, not formatting.
Deterministic by construction: literal rows, a pinned ``occurredAt``, a pinnedidempotency key, no clock, no randomness, no filesystem. (``occurredAt`` muststay pinned — ``create_governed_action_draft`` falls back to``datetime.now(timezone.utc)`` when it is omitted.)
Run with the sibling SDK on the path:
PYTHONPATH=../veritio/sdks/python/src python3 src/examples/governed/action-draft.py"""
import jsonfrom typing import Any
from veritio import create_governed_action_draft, define_entity
# Pinned verbatim from src/examples/governed/action-draft.ts. Do not "improve"# these values: their only job is to be the same inputs the TypeScript fixture# drafts, so the two expected outputs can be diffed field by field.GOVERNED_SCOPE = {"tenantId": "org_acme", "environment": "production"}
SUBSCRIPTION = define_entity( authority="acme-billing", entity_type="subscription", schema_ref="acme://schemas/subscription@3", field_set_ref="acme://fieldsets/subscription-governed@1", identity=lambda row: row["id"], fields={ "id": {"capture": "full"}, "plan": {"capture": "full"}, "seatCount": {"capture": "full"}, "status": {"capture": "full"}, # PII stays out of evidence as a value; only its digest is committed. "accountEmail": {"capture": "content_digest"}, # Ungoverned operator prose never enters the commitment or changed paths. "internalNotes": {"capture": "omit"}, },)
BEFORE: dict[str, Any] = { "id": "sub_9f31", "accountEmail": "billing@acme.example", "plan": "team", "seatCount": 12, "status": "active", "internalNotes": "renewal call scheduled",}
AFTER: dict[str, Any] = { **BEFORE, "plan": "enterprise", "seatCount": 25, "internalNotes": "upgrade approved on the renewal call",}
def build_subscription_upgrade_draft() -> dict[str, Any]: """Build the pinned upgrade draft shared by the docs page and this fixture.
Kept as a named function so the documentation can show one call site while CI re-runs the identical derivation. Every argument matches ``buildSubscriptionUpgradeDraft`` in the TypeScript twin, including ``occurredAt`` and ``idempotencyKey``, because both feed hashed evidence: ``occurredAt`` is stamped on every drafted event and edge, and ``idempotencyKey`` seeds the change/activity ids and the tenant-scoped idempotency hash. """ return create_governed_action_draft( { "scope": GOVERNED_SCOPE, "entity": SUBSCRIPTION, "before": BEFORE, "after": AFTER, "actionType": "subscription.upgraded", "activityType": "billing.plan_change", "initiatedBy": { "authority": "acme-billing", "kind": "principal", "type": "user", "id": "usr_owner", }, "performedBy": { "authority": "acme-billing", "kind": "principal", "type": "service", "id": "svc_billing_api", }, "producer": { "authority": "acme-billing", "kind": "principal", "type": "service", "id": "svc_billing_api", }, "occurredAt": "2026-08-09T10:00:00.000Z", "idempotencyKey": "subscription.upgraded:sub_9f31:req_7c2a", "mutationBinding": "same_transaction", } )
def main() -> None: """Print the governed-action parity report for the pinned upgrade.
Kept free of any ambient input (clock, environment, argv) so the program is byte-identical across runs and machines; the documentation pipeline diffs this stdout against expected/action-draft-python.txt, and a reviewer diffs that file against the TypeScript twin's expected output. """ draft = build_subscription_upgrade_draft() change_id = draft["changeRef"]["id"] id_seed = change_id[change_id.rfind("_") + 1 :] entity_id = draft["entityRef"]["id"] commitment = draft["revision"]["stateCommitment"]
output = { "derivedIds": { "changeId": change_id, "activityId": draft["activityRef"]["id"], "entityId": entity_id, "revisionId": draft["revision"]["ref"]["id"], "idSeed": id_seed, # Both ids are `<prefix>_<entityType>_<entityId>_<seed>` off the same # sha256(tenantId:idempotencyKey) seed, so replay is idempotent. "changeAndActivityShareOneSeed": ( change_id == f"chg_subscription_{entity_id}_{id_seed}" and draft["activityRef"]["id"] == f"act_subscription_{entity_id}_{id_seed}" ), }, "changedPaths": draft["revision"]["changedPaths"], "stateCommitment": { "algorithm": commitment["algorithm"], "canonicalization": commitment["canonicalization"], "schemaRef": commitment["schemaRef"], "fieldSetRef": commitment["fieldSetRef"], "digest": commitment["digest"], "committedFields": sorted(commitment["fields"].keys()), # `internalNotes` is declared `omit`, so it is absent from the # commitment and can never become a governed changed path. "omittedFieldPresent": "internalNotes" in commitment["fields"], "accountEmailCommitment": commitment["fields"]["accountEmail"], }, "eventActions": [event["action"] for event in draft["events"]], "edgeRelations": [edge["relation"] for edge in draft["edges"]], "draftIsInputOnly": { "note": "create_governed_action_draft returns evidence INPUTS, not persisted records. Append draft['outboxEntry'] through a conforming AuditStore inside the same mutation to make it evidence.", "eventsCarryStoreAssignedSequence": any( "sequence" in event for event in draft["events"] ), "eventsCarryRecordHash": any("hash" in event for event in draft["events"]), "outboxMutationBinding": draft["outboxEntry"]["mutationBinding"], "outboxSchemaVersion": draft["outboxEntry"]["schemaVersion"], }, }
print(json.dumps(output, indent=2))
if __name__ == "__main__": main(){ "derivedIds": { "changeId": "chg_subscription_sub_9f31_312ca4bde590b545", "activityId": "act_subscription_sub_9f31_312ca4bde590b545", "entityId": "sub_9f31", "revisionId": "rev_subscription_sub_9f31_a71e2768812c_fe34f6d0", "idSeed": "312ca4bde590b545", "changeAndActivityShareOneSeed": true }, "changedPaths": [ "/plan", "/seatCount" ], "stateCommitment": { "algorithm": "sha256", "canonicalization": "veritio-json-v1", "schemaRef": "acme://schemas/subscription@3", "fieldSetRef": "acme://fieldsets/subscription-governed@1", "digest": "sha256:a71e2768812cca4e3609ce7e24460967d253e1ddab13d9a8898f4fd31a3564cb", "committedFields": [ "accountEmail", "id", "plan", "seatCount", "status" ], "omittedFieldPresent": false, "accountEmailCommitment": { "captureMode": "content_digest", "digest": "sha256:aa69b0bc0b21a0ca7b5b5d1beb7556eb7d598f21d638db3eb9770ddd4fb30e45" } }, "eventActions": [ "change.declared", "activity.recorded", "entity.revision.created" ], "edgeRelations": [ "has_activity", "has_output", "performed_by", "generated" ], "draftIsInputOnly": { "note": "create_governed_action_draft returns evidence INPUTS, not persisted records. Append draft['outboxEntry'] through a conforming AuditStore inside the same mutation to make it evidence.", "eventsCarryStoreAssignedSequence": false, "eventsCarryRecordHash": false, "outboxMutationBinding": "same_transaction", "outboxSchemaVersion": "2026-06-23" }}Three invariants are visible in that output.
One seed drives both identifiers. The change id and the activity id are both <prefix>_<entityType>_<entityId>_<seed> over the same sha256(tenantId:idempotencyKey) digest, so a replayed mutation reproduces the same change and activity pair in Python that it does in TypeScript instead of forking the evidence graph per SDK.
define_entity is the only place field capture is decided. internalNotes is declared omit, so it never enters the state commitment and can never become a governed changed path — omittedFieldPresent is false and changedPaths contains only /plan and /seatCount even though the operator prose also changed. accountEmail is declared content_digest, so the commitment carries a digest envelope rather than the address.
A draft is input, not evidence. eventsCarryStoreAssignedSequence and eventsCarryRecordHash are both false. The draft has no sequence and no record hash because nothing has been appended yet. Python cannot close that gap in-process: there is no conforming AuditStore in this package, so draft["outboxEntry"] has to be written by your own transaction and drained to a store a TypeScript process owns, or to the hosted ingest path. The mutationBinding of same_transaction is a declaration about how you persist it, not something the SDK enforces.
create_governed_action_draft raises when no governed field changes, when a required ref is missing, and when a keyed_digest field is declared without digestKeys.keyedDigest. Pin occurredAt in tests: when omitted it falls back to datetime.now(timezone.utc) and the draft stops being reproducible.
Templates: same count, one different name
Section titled “Templates: same count, one different name”Python exports 26 event builders, the same 26 TypeScript exports, grouped by audit_template_sets into auth, organization, data, agent, and code. They are a convenience layer over create_audit_event, and they are not pinned by conformance fixtures. Nothing in CI would notice if one language’s template drifted in the metadata it stamps.
One name genuinely diverges:
TypeScript episodeStartedTemplateGo EpisodeStartedTemplatePython activity_episode_started_templateThe emitted event is the same. A mechanical camelCase-to-snake_case translation will miss it, which is exactly the failure mode to expect when porting a TypeScript agent-instrumentation module. The builder stamps activityEpisodeId as a reserved, un-shadowable grouping key, along with authSessionId and authContextId in reserved metadata that caller-supplied metadata cannot override.
Failure behavior
Section titled “Failure behavior”The SDK raises rather than degrading. create_audit_event rejects missing identities and malformed actions. create_evidence_edge rejects relations outside the supported vocabulary and entity types outside the evidence entity set. create_evidence_commit rejects a non-positive sequence and a previousCommitHash that is neither None nor a sha256: digest. canonical_json rejects non-finite numbers instead of inventing an unstable representation for them.
Never replace one of those exceptions with an empty tenant, a default actor, or a partially normalized dictionary. An event that was recorded with a substituted identity is worse than an event that was never recorded, because the chain will verify and the claim will be wrong.
Verify your checkout
Section titled “Verify your checkout”Run the upstream suite against the pinned source:
PYTHONPATH=sdks/python/src python3 -m unittest discover -s sdks/python/testsThen run bun run verify:examples in this website to re-execute both fixtures on this page and diff their stdout against the committed expected output.
Where to go next
Section titled “Where to go next”Read Cross-language parity matrix for the full capability table across all three SDKs, including which behaviors are pinned by shared fixtures and which are merely present. Use FastAPI for a complete Python application example. If your architecture needs the conforming authoritative-store interface or a record-chain verifier in-process, take the TypeScript path or implement the missing capability in the OSS repository before documenting it as available.