Skip to main content

sanning-anchor

Produce verifiable evidence from your Python agents: hash locally, anchor a signed commitment, and hand an auditor a pack they verify offline. Nothing in this package needs a wallet, a chain identity, or a Sanning account to check.

Node is not required at any point. The bundle command that assembles a hand-over pack is a Python console script, and so is everything before it.

  • The path from a first conversation with Sanning to a verified event is at docs.sanning.io.
  • The wire format, the envelope profile and the pack body type are specified in documents that ship inside sanning-proof, beside the kernel that implements them.

Install

pip install sanning-anchor

Access to the console comes through a conversation with Sanning; there is no self-service sign-up. Once your organization has a console login, create an API key at console.sanning.io. Give the key agent:enroll and anchor:write to anchor, and anchor:read as well if the same key will later build a hand-over pack. The console pre-checks the first two. A key holding only anchor:write anchors nothing, because the plane refuses an envelope whose signing key the organization has not enrolled; a key holding no anchor:read anchors correctly and then fails at hand-over.

Anchor an event

import json
import os

from sanning_anchor import Anchorer, FsLogStore, LocalEd25519Signer

anchorer = Anchorer(
    # Writes to a permanent public record. Use "dev" until you mean it.
    environment="production",
    # Your Ed25519 identity key. Keep the seed: a different seed is a different agent.
    signer=LocalEd25519Signer.from_seed_hex(os.environ["SANNING_SIGNING_SEED"]),
    subject={"type": "agent", "agent_id": "claims-triage"},
    api_key=os.environ["SANNING_API_KEY"],
    log_store=FsLogStore("./logstore"),
)

decision = {"claim_id": "c-1", "outcome": "approved"}  # your own data

result = anchorer.anchor(
    event_type="claims.decision",
    # Hashed in this process. These bytes are never sent to Sanning.
    content=json.dumps(decision).encode(),
    metadata={"reviewer": "queue-3"},
)

anchorer.close()

result.event_id  # the id this process minted
result.event_bytes  # retain these: the bytes payload_hash commits to

Replace the following:

  • SANNING_SIGNING_SEED with a 32-byte Ed25519 seed, 64 hex characters, that your service keeps across restarts. An agent's identity is this key, not its name. bytes.fromhex accepts either case; the TypeScript SDK's fromSeedHex takes lowercase only.
  • SANNING_API_KEY with the key from the console.
  • claims-triage with the identity you want the evidence attributed to.

@sanning/anchor takes the same arguments under TypeScript naming and writes the same bytes, so a mixed fleet hands over one pack.

Chains need a durable store

anchor(chain="orders") links each record to the one before it under that chain key, and keeps the head in the anchorer's store. The default MemoryStore loses its heads on a restart, so the next record on the same chain key starts again at GENESIS: one chain with two roots, a fork whose links all verify. sanning-proof reports it as the chain-second-root finding, which does not change the verdict.

Use a durable store with one writer for any chain key that outlives one process. A durable store does not make two writers safe, because the store has no compare-and-set: two instances on one chain key fork the same way. In production, anchor(chain=...) on the default store warns once per anchorer. If every chain key lives only as long as the process (the LangChain handler's per-run key, for example), pass store=MemoryStore() to say so and the warning stays quiet.

Enrollment is automatic

The example writes no enrollment step, and that is the whole of it: on the first anchor(), the SDK proves possession of your signing key to the control plane, and your agent appears on Fleet under agent_id. One organization key covers the whole fleet: no per-agent credential, no enroll script. It is not optional politeness, because the plane refuses an envelope signed by a key your organization has not enrolled (SIGNING_KEY_NOT_ENROLLED, HTTP 403).

Three properties are load-bearing:

  • The challenge is shape-checked before it is signed. Enrollment is the one moment your identity key signs bytes the server chose, and that key also seals your envelopes, so the SDK refuses anything that is not a nonce rather than trusting Sanning not to send an envelope pre-image. Sanning is never in the trust path, including here.
  • A different key for an agent_id your organization has enrolled is a rotation, and it needs the retiring key's counter-signature (previous_signer=). It is never inferred from a refusal: that is what stops a leaked organization key silently taking over an identity that is already producing evidence.
  • Verification never depends on the roster. Enrollment is the plane's door policy, not the trust path. Every signed record verifies offline against sanning-proof, enrolled or not.

Enrolling out of band at deploy time instead? auto_enroll=False turns the call off, and enroll() is exported for you to drive:

from sanning_anchor import enroll

enroll(
    api_key=os.environ["SANNING_API_KEY"],
    agent_id="claims-triage",
    signer=signer,
)

Behaviour is identical to the TypeScript SDK, pinned by a shared conformance oracle that calls neither of them.

What never leaves your process

Your content does not. It is hashed locally and only the hash goes into the signed envelope. The control plane is content-blind by construction rather than by policy: it never receives the bytes, so it cannot disclose them.

event_bytes is your retention obligation. It is what payload_hash commits to. Sanning never holds it, so losing it leaves you holding a commitment to something you can no longer produce.

No Arweave wallet, no chain identity, no data item. Placement is Sanning's act, which is why this package holds no blockchain code at all, and why it is a few hundred lines rather than a chain client.

Retain what you anchored

Pass log_store= and the SDK writes both halves an auditor needs before the event is anchored: the content you handed it, and the canonical event record the envelope commits to. If the store write fails, nothing is anchored. There is deliberately no best-effort mode, because under this write path nothing else holds those bytes.

Beside each record it also writes the signed envelope over it, at envelopes/<payload_hash>, so a record read back from your own store carries its signature and its signed_at. That time is the one your agent declared when it signed: it is never trusted for ordering and never used as proof. A store written before this change holds no envelopes and reads exactly as it did.

FsLogStore is the development destination. For production, sanning_anchor.s3 supplies an S3-compatible object store behind the same seam, and the on-disk shape is a versioned contract, specs/log-store.md in the sanning-proof package, so an auditor holding nothing but the directory can resolve it without a connector and without Sanning.

--logs takes a local store root or an s3:// URL. Reading a bucket needs SANNING_S3_ENDPOINT, SANNING_S3_ACCESS_KEY_ID and SANNING_S3_SECRET_ACCESS_KEY in the environment, and a missing one is named individually. There is no flag for them: a credential in argv lands in shell history. The reader credential is enough, s3:GetObject and s3:ListBucket.

Trace a LangChain agent

pip install "sanning-anchor[langchain]"
from sanning_anchor import AnchorCallbackHandler

with AnchorCallbackHandler(anchorer) as handler:
    agent.invoke(inputs, config={"callbacks": [handler]})

Every chain, model, tool and retriever step is anchored, with LangChain's run_id and parent_run_id tree committed alongside a per-run seq and prev_event_id. A missing event leaves a hole in seq and a moved one breaks the chain, so the trail is deletion-evident and reorder-evident.

Four things decide whether your integration is correct, so they sit here rather than in a guide:

  • The with block raises on a gap. Leaving it out anchors what it can and walks past what it cannot, and you find out when an auditor asks. Use raise_on_gaps=True to stop the agent instead.

  • The whole step is committed, prompts, outputs, tool input and output. It stays with you. on_event watches what is committed and cannot change it.

  • A file a tool returns gets its own record, committed to the file's own bytes, with no setup: its event.content_hash is the file's SHA-256, and the bytes go to the log store as the record's content, like any other content. metadata.artifact names the file and carries no hash. A file the SDK cannot read becomes a langchain.artifact_unreadable event with its reason, because silence is how files go unrecorded on a run that reports success.

  • Transient failures are retried; a 4xx is not. The plane has said the envelope is wrong, and repeating it is a slower failure rather than a recovery.

  • The setup record. Pass rule_documents=[...] and each document's sha256 hex digest joins the first event of each root run, beside the prompt, the model and the tools it already commits. Names stay yours: a path is read only long enough to hash it, and only the hash is ever recorded.

  • The closing record (anchor#194). A langchain.run_closed event is written once per root run, automatically, when close() runs -- no option gates it. It states how many records that root run anchored (the anchorer accepted them) and their ids, even 0: a root run that anchored nothing still gets one stating the zero, which is a different wire shape from an SDK too old to write this event at all. Once the handler is closed, every later event is refused, so nothing lands after a root's closing record. A close() that returns before the closing records are written raises ClosingNotDeliveredError (an AnchorError, from sanning_anchor), whatever raise_on_gaps says: the second of two concurrent calls waits at most 120 s for the first, and one called from inside an on_event hook does not wait at all. That includes one called from inside a closing anchor call (a hook on a closing record, a custom anchorer writing one): the closing that call is part of cannot be confirmed from inside it, so it raises every time, and retrying in a loop there only holds up the claiming close() and never succeeds. The hook guard logs it from a hook. From a custom anchorer the error leaves anchor(), and that root's run_closed comes back unbuilt (complete=False) unless the anchorer catches it.

  • The file role (anchor#194). A tool's returned object can name role ("input" or "output"), source and/or destination alongside its path or bytes, and all three land on the artifact record unchanged. Absent unless the tool names them.

  • The promoted fields name the model, the tool and the step (anchor#270, anchor#268). Beside the run linkage (run.run_id, run.parent_run_id, run.root_run_id, run.seq, run.prev_event_id), each record carries, where LangChain hands it over: run.name (the step, by LangChain's own naming rule), run.client (the client class, such as ChatOpenAI), and under gen_ai the OpenTelemetry GenAI names, pinned to v1.41.0: gen_ai.request.model, .temperature, .max_tokens, .seed, .top_p; gen_ai.provider.name (the service that served the call, in OTel's well-known values plus openrouter; see below); gen_ai.response.model, .id, .finish_reasons; gen_ai.usage.input_tokens, .output_tokens, .reasoning.output_tokens; gen_ai.tool.name (the serialized tool's own name, never the run name a caller sets) and gen_ai.tool.call.id; gen_ai.prompt.name; and gen_ai.agent.version. openai.response.system_fingerprint sits in OpenAI's own namespace. Every record carries otel.scope: sanning-anchor, its version, and https://opentelemetry.io/schemas/1.41.0. v1.41.0 is the last core OTel release where these names are current; from v1.42.0 core marks them moved to open-telemetry/semantic-conventions-genai, which has no release yet, and Sanning moves the pin deliberately once it does. A field LangChain does not hand over is absent, never empty: no response id when the provider put none on the message (LangChain's own lc_run-… id is never written as the provider's), no token counts when neither usage_metadata nor llm_output has them, no fingerprint from a non-OpenAI-compatible provider. Records anchored before this change keep the old names (model.id, model.id_returned, model.provider, run.tool_call_id); this SDK writes only the new ones.

    handler = AnchorCallbackHandler(
        anchorer,
        record_names=False,          # keep run.name and gen_ai.tool.name out
        agent_version="2026.10.1",   # gen_ai.agent.version on every record
    )
    agent.invoke(
        inputs,
        config={"callbacks": [handler], "metadata": {"gen_ai.prompt.name": "underwrite-v3"}},
    )
    

    record_names=False exists because a record's metadata is visible whenever the record is, even in a pack whose content is withheld, and a tool's name can say more than you meant (lookup_customer_ssn). It omits both names and nothing else; they stay in the committed content, hashed and withheld like the rest.

For the event vocabulary and worked examples, see Trace a LangChain agent.

Hand over a pack

When a compliance person asks for one agent's evidence over one period, the SDK assembles it in one command, with no Node installed:

export SANNING_API_KEY=ANCHOR_READ_KEY   # the key needs the `anchor:read` scope

sanning-anchor bundle \
  --agent claims-triage \
  --from 2026-06-01 --to 2026-06-30 \
  --logs ./logstore \
  --out claims-triage-2026-06.zip

Replace ANCHOR_READ_KEY with a key from the console carrying anchor:read. There is no flag for it: a credential in argv lands in shell history, and this is a command people paste into tickets. A key missing the scope is reported as a scope to widen, never as an absence of evidence.

Flag What it names
--agent The agent's agent_id, as committed in each event's subject. There is no --producer alias.
--from Period start, inclusive. A date or an ISO instant. Required unless --case is given; then optional, and given together with --to.
--to Period end. A YYYY-MM-DD names a whole day and is included; an ISO instant is exclusive. Required unless --case is given.
--logs Your sanning.logstore/v1 store root: the local directory holding content/ and records/, or s3://bucket or s3://bucket/prefix.
--out Where to write the pack. A .zip suffix is appended when absent.
--base-url The control plane. Defaults to the hosted plane, or $SANNING_BASE_URL.
--allow-unstamped Assemble even when records in the period are anchored but not yet stamped. Without it the verb refuses. Takes no value.
--case Select a case's records and declare the pack about it. The records are those the plane returns for the case's digest (case_id_hash), each kept only when its own signed context.case_id is the case; no window is derived. --from and --to are optional and narrow the same read; see One case, no window.
--handoffs-only With --case, pack only the case's hand-off records: the minimum-disclosure selection for handing another organization your side of a case.
--report-file Case file only: a file to put under report/. Takes a path, and may be given more than once.

sanning-anchor --help prints that reference, the exit codes, and the offline command that checks what the verb produced.

The window is the plane's clock, not yours. --from and --to are matched against each record's witnessed_at, which the plane sets when it accepts the envelope. That instant can be earlier than the local one you captured just before calling anchor(): 433 ms earlier on a measured production run. A record at the edge of your window then falls outside it, and the pack is short by that record while the command exits 0 and warns about nothing, which is the one outcome a pack's reader cannot detect. Widen the window by a few seconds on each side, and compare the pack's event count against what you anchored.

Pass prefix to one of S3ObjectStore and the ObjectLogStore wrapper, never both. Each of them prepends it, so the same value on both writes ts/ts/content/... while every reader looks under ts/content/..., and the writes and the reads disagree. bundle refuses correctly, with no record object in your log store, and that message does not name the cause.

That writes one zip: a signed sanning.stamp.evidence/v2 bundle.json beside logs/<event id>.json. Proofs come from the Sanning index; the raw bytes come from your log store and never left it. The verb verifies the whole pack offline before it writes anything, so a pack that would fail at the auditor is never produced (exit 1, no file).

A stamp settles after it seals. For the first minutes of its life an interval stamp has not reached the public record, and a pack resting on one cannot be verified offline by whoever receives it. The verb says so and exits 1, the same way it does for records that are not stamped yet, and the message names the interval it is waiting for: run the command again in a few minutes. Your records are anchored, retained and stamped throughout, and nothing about them is wrong.

It also refuses to assemble a short pack, in two cases, because a recipient cannot tell a short pack from a complete one:

  • Records whose record object is missing from your store make subject.agent_id unknowable, and one of them may be this agent's.
  • Records that are anchored but not yet stamped have no inclusion proof, so a pack built now omits them and says so nowhere inside itself. The stamper seals on an interval, and running again once it has is usually the whole fix. --allow-unstamped assembles anyway, and then the summary tells you the pack is short so you can say so in the cover note.

No signing key is required. Without one, the container is sealed with a one-time assembly key, and every event inside already carries its agent's own signature. With SANNING_SIGNING_SEED, the pack carries a signed declaration of what it holds.

One case, no window

--case selects a case's records, and --from and --to are optional with it:

sanning-anchor bundle --case case-4b19de \
  --agent assessor \
  --logs ./logstore --out case-4b19de.zip
  • The pack's records are the ones the plane returns for the case's label, kept on their own signed context.case_id. The SDK sends the case label (case_id_hash, the SHA-256 of the case id as 64 lowercase hex characters) beside every record it signs into a case. --case asks the plane for the records filed under that digest, pages to the end, and keeps each one whose own signed context.case_id is the case. It reads nothing else and derives no window. --from and --to (together or not at all) narrow the same read. The case id itself never leaves your machine.
  • The digest only finds candidates. It is the producer's own word and is not signed, so it selects nothing on its own: a row filed under it whose signed record names another case is left out and counted on the summary.
  • Records without the label are not found. The pre-release builds (sanning-anchor 0.9.0rc1, @sanning/anchor 0.15.0-rc.1) and a released 0.15.0 to 0.17.0 SDK with a custom one-argument submitter sent a record signed into a case without it. A custom client must accept the index keyword argument that carries the label (submit(envelope, index=None)): the Anchorer reads the signature of submit at construction and raises TypeError unless it has a parameter named index or takes **kwargs, rather than drop the label in silence.
  • A record in the window that names no case refuses a declared pack (the signing seed is set), and an undeclared pack leaves it out and says how many. Pass the case id when you anchor.
  • The pack's period is the span of the records it holds: the earliest to the latest signed_at among them, read from each signed envelope. The pack's records run oldest first by that signed time, never by a time the plane's list sends. It is not a window, so there is nothing to pad. The declaration's period.end is the exclusive bound, one millisecond after the latest record.
  • With no window the plane reads at most 92 days of the case. A case with records beyond it exits 1 rather than build a short pack (an earlier bundle run's declaration counts among the case's records, so a case re-bundled long after its first record can reach it), and --from/--to name the part you want. A plane that does not say how much of the case fell outside what it read is refused the same way. An empty selection exits 1 and writes nothing.
  • A case file (several --agent) selects the same way: each pack states its own records' span, and the case file's period line is the span of all of them.

Several agents, one case: the case file

Give --agent more than once, with --case, and the verb writes a case file (evidence-bundle.md §5.6): one pack per agent, each built, declared and verified exactly as a single pack is and sealed by that agent's own key, unpacked under packs/<agent>/ in one zip beside contents.json (an unsigned index of every file and its SHA-256) and HOW-TO-VERIFY.txt. Nobody signs the case file itself.

export SANNING_API_KEY=…
export SANNING_SIGNING_SEEDS=assessor=SEED_A,checker=SEED_B
sanning-anchor bundle --case case-4b19de \
  --agent assessor --agent checker \
  --logs ./logstore --out case-4b19de.zip

Replace SEED_A and SEED_B with each agent's 64-hex signing seed. SANNING_SIGNING_SEEDS holds one <agent>=<seed> entry per --agent, and SANNING_SIGNING_SEED is refused beside it. --logs is given once for every agent, or once per --agent in the same order. An agent whose pack is refused is listed in contents.json's absent[] with its key and the reason; an agent whose records are not stamped yet stops the whole command with exit 3. --report-file <path>, given any number of times, adds files under report/ beside a report/README.txt saying no record commits to them.

Every declared pack (SANNING_SIGNING_SEED set), single or in a case file, declares sanning.declaration/v2: each record is signed handoff (a record envelope-spec.md §4.1 governs, claimed in handoffs[]) or step. Each hand-off record's message travels as logs/<event id>.json, exactly JCS(message), the bytes its message_hash commits to. So a full pack reads cleanly as a case, and --handoffs-only is the minimum-disclosure selection for handing another organization your hand-off records alone.

Hand-offs between agents

create_handoff(anchorer=..., case_id=...) records a message that passes between two agents, over any transport: the sender's send writes handoff.sent, the receiver's receive verifies what arrived and writes handoff.received. Two additions, both offline:

  • How gen_ai.provider.name is decided (anchor#270). It is a signed claim about which service served the call, so it is written only from an endpoint this adapter positively observed, in the client library's own precedence. First, the base-URL field in the client's dumpd() kwargs: openai_api_base (ChatOpenAI), anthropic_api_url (ChatAnthropic), groq_api_base (ChatGroq), xai_api_base (ChatXAI), endpoint (ChatMistralAI). These clients resolve their own LangChain-level variables and the LangSmith gateway into that field at init (OPENAI_API_BASE, ANTHROPIC_API_URL/ANTHROPIC_BASE_URL, GROQ_API_BASE, XAI_API_BASE, MISTRAL_BASE_URL), so the field is what the client will call. When the field is unset, the variable the underlying SDK reads decides (OPENAI_BASE_URL, GROQ_BASE_URL), then the SDK's documented default. The provider is that endpoint's host: a ChatOpenAI pointed at OpenRouter says openrouter. In every other case the field is absent: a client class not listed (ChatDeepSeek serializes as not_implemented, with no kwargs), a host this package does not know (a local server, a proxy, the LangSmith gateway). run.client still records the class. The table is pinned by captures from the published clients in tools/langchain-callback-conformance/provider-captures.json. An endpoint chosen in code, such as a pre-built client=, is not in the serialized form; set the endpoint with the client's base_url instead.
  • receive(expected_sender=...) refuses a message whose signed sender key is not the one you name, with HandoffUnexpectedSenderError, before anything is recorded. Either case of a key is the same key.
  • handoff.confirm(sent=..., received_record=...) lets the sender check the receiver's own signed record (received.received_record, sent back over your transport) with the kernel's verify_envelope and read_handoffs. It returns a HandoffConfirmation with confirmed, reasons, outcome, receiver_public_key and readings, and reasons names every check that failed. A receipt is untrusted, so nothing about it raises.

outcome is a required keyword argument of receive: the agent decides, accepted, rejected or failed, then receive records that decision, before your agent acts on the message. There is no default: this SDK never signs a decision on your behalf. Omitting outcome is a TypeError from Python itself, since it is a required keyword argument; passing it anyway with None or a value outside those three words raises HandoffOutcomeRequiredError before anything verifies or is written. The spec has no word for "received, not yet decided", which is a real limitation Hand off between agents states in full, along with the refusal table. Both SDKs return the same reason names, graded against one recorded cross-language run.

Verify a pack

Verification lives in sanning-proof and needs no account, no key and no network. The case reader reads one pack or a whole case file, and bundle prints it as its next step (the case command reads a v2 pack from sanning-proof 0.13.0):

unzip pack.zip -d pack
sanning-proof case pack

The library call grades one bundle on its own:

import json, pathlib
from sanning_proof import verify_evidence_bundle

pack = pathlib.Path("pack")
verdict = verify_evidence_bundle(
    json.loads((pack / "bundle.json").read_bytes()),
    content={p.stem: p.read_bytes() for p in (pack / "logs").glob("*.json")},
)
verdict.status  # "verified"

Whoever receives your pack does not need this SDK. For what a verdict proves, and the four things it does not claim, see Verify.

Parity with the TypeScript SDK

sanning-anchor and @sanning/anchor produce the same signed bytes for the same event, and the same signed pack body, byte for byte, for the same pack, so a mixed fleet hands over one pack. Both are gated against the same pinned corpus for the envelope, and against an oracle that imports neither SDK for the hand-over pack, so neither language can pass by agreeing with the other.

The LangChain adapters commit JSON.stringify(payload), which json.dumps is not, so this package reimplements it against a cross-language corpus generated from the real JSON.stringify.

One case refuses rather than diverges: an integer outside ±(253−1), a set, a reference cycle. JavaScript would round the first one, and two different 64-bit trace ids can round to the same double, which is a false integrity verdict rather than a formatting difference. So it fails at anchor time, while you can still fix it, rather than at verification, when the record is already sealed. Convert the value before anchoring it.

Development

pip install -e ".[dev]"
black --check src tests
pytest -q

MIT licensed.

Metadata

Release files for sanning-anchor 0.17.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sanning-anchor 0.17.1
File Size Uploaded
sanning_anchor-0.17.1.tar.gz 361.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sanning-anchor 0.17.1
File Interpreter ABI Platform
sanning_anchor-0.17.1-py3-none-any.whl Python 3 none any Details

Total release size: 691.4 kB

Release files / sanning_anchor-0.17.1.tar.gz

Download URL sanning_anchor-0.17.1.tar.gz
Size 361.3 kB
Tags Source
SHA-256 checksum
How to use checksums
5d9c7f9a18d1d6b63289658da5848e4a96daba738a8832009463306d8d17ad51
BLAKE2b-256 checksum
How to use checksums
eba6c94a8d15cb950edbbd8757ca3af1919dfd60eb0f9d3304f3560ad4a78b4c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release files / sanning_anchor-0.17.1-py3-none-any.whl

Download URL sanning_anchor-0.17.1-py3-none-any.whl
Size 330.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
da15bff7dbee0f260d70676ace2b8668a58802c1670095ea7c02ecde0d150f9e
BLAKE2b-256 checksum
How to use checksums
141648886d83f6c954bd8d25680dea61a0fc951d50d7a90f5c37caf4faec4761
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release history Release notifications | RSS feed

0.19.1

2 release files

0.18.0

2 release files

This release

0.17.1 This release

2 release files

0.16.2

2 release files

0.16.1

2 release files

0.16.0

2 release files

0.15.0

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page