Skip to main content
Sifr can embed one uv-created CPython environment in a compiled application. Use it to call installed Python packages while keeping Sifr contracts: fallible work returns Result, Python objects are opaque and non-send by default, native extensions need explicit trust, and zero-copy APIs never silently copy. This is not Python source compatibility and not a generic dlopen layer. Prefer sifr.* for Sifr code; use sifr.python only when you intentionally cross into Python packages. For library-first walkthroughs, start with schwifty, NumPy, or kafka-python.

Package Setup

The root application owns the Python environment. Sifr verifies and consumes it; it does not run uv sync, install packages, or fall back to host-global Python.
For a normal uv project, Sifr finds the nearest ancestor with both pyproject.toml and uv.lock, then uses its .venv and platform interpreter. Non-standard layouts can override venv, pyproject, lock, or interpreter in the root [python] table. Sifr runs read-only uv lock --check --offline and probes the live interpreter; it never creates or mutates the environment. Libraries may declare required import roots, but they do not choose the interpreter or venv:
requires-imports is only for raw or dynamic imports whose roots cannot be derived from static declarations or package-local bridge imports. Sifr normalizes all contributions into one per-root set and keeps every source for diagnostics. Sifr rejects missing uv environments, dependency packages that select or authorize an environment, non-CPython interpreters, free-threaded CPython, stale or missing project metadata, missing required imports, and trusted native roots that fail to load.

Trust Boundary

Python import roots and native extension roots are separate trust decisions. Native extensions can abort the process, create threads, release the GIL, or call back into Python/Sifr. That is the explicit exception to Sifr-attributable no-panic guarantees. Root apps may use wildcard Python trust during local control. Dependencies may publish requirements, but cannot select an environment or authorize execution. Native trust for a root that is not required is rejected as stale policy.

Check And Doctor

Inspect the declaration-first Python plan before building:
Both commands use frozen package resolution and the same driver plan as normal check/build. Reports include graph and source-content snapshot digests for the inputs that were checked. Resolution rules:
  • A library without complete root authority is valid to inspect; unresolved obligations stay deferred.
  • Ordinary sifr check uses the same deferral decision.
  • A library with authorized imports and either explicit [python] paths or a discoverable uv project resolves immediately.
  • Any package with runnable applications resolves immediately.
  • Failures match ordinary sifr check diagnostics for the same snapshot.
The language server uses the same lowered plan and driver probe. Completion and hover show target, status, and protocol guidance; go-to-definition navigates to the checked Sifr declaration. Invalid, unsupported, or untyped declarations are never shown as verified. Cache invalidates on sifr.toml, custom pyproject/lock paths, sifr.python-bindings.json, sifr.python-certifications.json, or interpreter changes. sifr python doctor emits deterministic patch-like suggestions for the root [python] and/or [trust] entries a consumer must provide. Neither command writes a manifest, grants trust, creates a venv, installs packages, or runs uv sync.

Runtime Semantics

When Python interop is used, the generated binary initializes CPython once before user main, using validated probe metadata: executable, venv prefix, site-packages, sys.path, CPython version, SOABI, extension suffixes, platform, pointer width, and libpython when discoverable. Rules:
  • CPython main interpreter only; no subinterpreters.
  • No normal shutdown finalization with Py_FinalizeEx.
  • Reinitialization with the same config is allowed; conflicting config is rejected.
  • Every CPython API operation that needs the GIL acquires it.
  • Decref and resource release happen while holding the GIL.
  • py.Object, buffers, Arrow capsules, DLPack tensors, and callbacks are tracked for ownership and release diagnostics.

Typed Declarations

Use typed declarations for ordinary package calls. The Sifr signature is the conversion contract; every failure remains a checked Result:
Every declaration error channel must use the canonical PythonError contract: exactly the string fields message, kind, exception_type, traceback, and context. Importing PythonError from sifr.python supplies that contract for ordinary, coroutine, callback, context, and zero-copy declarations. Adaptation code belongs under the owning package’s src/python_bridges/. A source target such as bridge.identifiers.parse_gtin resolves only against that package’s inventoried bridge modules:
Bridge rules:
  • Ordinary static imports are inventoried; dynamic imports are rejected.
  • Third-party roots enter the requirement set and need root [trust].python.
  • Bridge sources and inventory are package archive inputs.
  • Generated binaries embed the selected graph under isolated __sifr_bridge__.p_* namespaces.
  • The reserved loader installs before user main; binaries do not read bridge files at runtime.
  • Two dependencies can own the same bridge module path without collision.
The dynamic Object handle is the sealed identity from sifr.python. A local record named Object keeps ordinary record-conversion semantics when it has supported fields; it cannot impersonate the sealed Python handle.

Dynamic Object Escape Hatch

Use the sealed dynamic object API only when a boundary cannot be a typed declaration or package-local bridge:
Every Python boundary operation returns Result. Python exceptions become structured py.PythonError values (type, message, traceback, operation context, and conversion/resource/zero-copy metadata where relevant). They do not unwind through Sifr user code. Conversion helpers:
  • from_value[T] / to_value[T] use the same closed set as typed declarations: primitives, supported lists, tuples, dict[str, T], closed records, options, opaque declarations, and sealed Object.
  • kwarg[T] converts into the (str, Object) call shape.
  • Object.get_attr, get_item, call, and call_method are checked method-style operations over that identity.
Ordinary Object values release when Sifr ownership ends, including return and error paths. Compatibility helpers such as close remain available for early release, but normal code does not need reverse-order close chains. Raw coroutines submitted with run_coroutine_blocking use the application-owned Python event loop; they do not create a per-call loop.

Authoring Typed Bindings

sifr python bind creates symbol-selective, reviewable Sifr declarations from Python typing information. Whole-module generation is unsupported:
Resolution order:
  1. Package-local --override files
  2. Selected --stub-package distributions
  3. Installed py.typed inline sources
  4. Package-local --external-stub files
  5. Safe runtime introspection
Every selected symbol records its winning source, source hash, distribution version, SOABI, environment identity, and generated source digest in sifr.python-bindings.json. Generated .sifr files, the artifact, and consumed package-local typing sources are package archive inputs. Generation fails closed on Any, bare object, unknown overloads, callable or generic shapes without a Sifr contract, missing annotations, unresolved types, and optional positional-only parameters the call plan cannot represent. It never substitutes dynamic Object or overwrites an unrelated package file. If the environment identity changes while adding or replacing one module, every retained binding is re-probed before the artifact adopts the new identity. bind --check is frozen and read-only: it reruns resolution and generation in memory and rejects typing, environment, distribution, source, or generated-file drift. Ordinary package check/build also validates checked-in outputs and local typing hashes, and includes binding identity in its build cache key.

Blocking And Async

Every public sifr.python operation is @blocking_io. Direct Python calls in async Sifr code are rejected unless offloaded through Sifr’s blocking offload primitive.
Libraries with genuine coroutine APIs use typed async declarations. The generated application owns one asyncio loop on one dedicated thread; declaration calls submit to that loop without blocking a Sifr executor thread:
Async declaration rules:
  • @python.coroutine is valid only on async def.
  • Inputs and outputs use the same recursive typed conversion contract as sync declarations.
  • Opaque values stay non-send.
  • cleanup=async_close requires exactly one consuming aclose(own self) coroutine.
  • The value cannot be abandoned, closed twice, or reused after close.
Cancellation:
  • Cancelling a Sifr task cancels its exact asyncio task and waits for Python finally cleanup first.
  • A terminal CancelledError caused by that request is the Sifr cancellation cause, not a catchable PythonError.
  • If Python suppresses cancellation, its later return or different exception wins.
  • Runtime shutdown drains registered tasks, traverses a reserved async-cleanup ordering slot, then stops and joins the loop thread.
Typed Python async context managers use the same owned loop and declare both protocol methods:
cleanup=async_context rules:
  • Must be consumed by async with.
  • Enter and exit run on the application-owned Python loop.
  • A truthy __aexit__ may suppress an originating Python exception.
  • Ordinary Sifr errors, timeout, cancellation, and runtime faults remain primary.
  • Cancellation waits for Python finally and masked async exit to finish.
  • Exit runs exactly once on fallthrough, return, error, and cancellation paths.
py.run_coroutine_blocking is also @blocking_io. It is for Python-owned coroutine objects and returns only after Python finishes.

Resources

Python resources must be closed or released explicitly when correctness depends on cleanup:
Typed context protocols, callback owners, buffers, Arrow resources, and DLPack tensors carry cleanup in the declaration contract. Double close/release returns deterministic resource-state errors for types that are not intentionally idempotent.

Callbacks

Attach a callback contract to the parameter that becomes the Python callable. The ordinary Sifr signature remains the conversion contract:
Retained owners must have deterministic close, async_close, context, or async_context cleanup. Owner shutdown unregisters first, rejects new entries, drains accepted invocations, and releases captures exactly once. Foreign and asyncio callbacks require concurrency=serial | parallel. Serial recursion fails with a stable callback error instead of blocking on its own lock or queue. Handler errors cross Python as SifrCallbackError and return through the declared Result channel even if Python catches the exception. If Python also fails, the Python error stays primary and the first handler failure is secondary evidence. Lower-level py.local_callback and py.threadsafe_callback remain available for explicit dynamic object code.

Zero-Copy Interop

Zero-copy APIs reject unsupported dtype, shape, stride, device, mutability, malformed capsule, double-consumption, and copy-required paths. Explicit copy APIs are separate and never claim view semantics.

Buffers

Typed buffer declarations cover objects that implement Python’s buffer protocol:
Decorator targets may be import-root callables, package-local bridge callables, or Self on an opaque receiver.
  • Self acquisition is read-only.
  • Writable declarations must use a producer that returns a fresh or ownership-transferred exporter.
  • Writable-producer parameters that can carry an existing python.Object or opaque Python identity must use own.
  • access=read | write and layout=any | c_contiguous | f_contiguous are validated during acquisition with element format, item size, dimensions, shape, strides, and suboffsets.
  • Supported element types are fixed-width and pointer-width signed and unsigned integers plus float.
python.Buffer[T] is affine and non-send. It retains its exporter and performs exactly one PyBuffer_Release on explicit release() or automatic drop. Writable access requires an exclusive mut receiver. read and write are bounds checked; copy_slice is an explicit copy. No borrowed Rust or Sifr slice can outlive the buffer.

Arrow

Typed Arrow declarations return one of five affine, non-send resources: python.ArrowArray, python.ArrowSchema, python.ArrowStream, python.ArrowDeviceArray, or python.ArrowDeviceStream. The return type selects the protocol and capsule shape:
Arrow ownership:
  • schema=parameter(name) names a required keyword-only own python.ArrowSchema parameter.
  • Sifr transfers that exact schema capsule to the Arrow protocol, but never forwards it to the producer call.
  • Arrays own their schema/data capsule pair; device arrays own the schema/device-data pair.
  • Owned Arrow values can cross another Python declaration only through an own parameter.
  • That own parameter commits the move before the call and reconciles full, partial, or absent capsule consumption afterward.
Arrow declarations require package-authored executable no-copy certification for the exact target, environment, distribution versions, schema mode, and runtime producer module/type:
The first command writes or updates package-root sifr.python-certifications.json. The second is read-only and reruns every recorded fixture, rejecting environment, fixture, producer, distribution, schema, pointer-identity, release-count, or copy-result drift. The artifact and package-local fixtures are archive and build-cache inputs. There is no Arrow copy switch or uncertain-producer fallback.

DLPack

Typed DLPack declarations return affine python.DlpackTensor[T] resources:
Producer targets may be import-root callables, package-local bridges, or Self. Device and stream:
  • device=cpu | cuda | any never requests a device change.
  • CPU may use stream=none.
  • CUDA and any require stream=parameter(name) — a required keyword-only borrowed python.DlpackStream from @python.dlpack.stream.
  • Runtime acquisition validates the stream family/id before the one permitted __dlpack__ call.
  • A CPU producer always receives stream=None, including when device=any validated matching CPU stream metadata.
Transfer rules:
  • Sifr passes copy=False and max_version=(1, 0) without an old-signature retry.
  • Legacy or compatible 1.x capsules are accepted.
  • Acquisition validates copy flags, dtype/lanes, device, dimensions, shape, strides, byte offset, and deleter state.
  • Element types are the closed Sifr scalar set: fixed-width integers, float (DLPack float64), and bool.
  • Sifr transfers the producer’s managed tensor without reading or mutating its payload, so READ_ONLY is preserved for the consumer.
  • The source capsule is immediately marked used.
  • Passing own python.DlpackTensor[T] creates a fresh consumer capsule and commits the move even if the Python call later fails.
  • An unconsumed capsule invokes its producer deleter exactly once.
DLPack producer claims use the same certification discipline as Arrow:
The fixture must prove pointer identity, no copy, declared device and stream policy, exact distribution versions, and exactly one observed managed-tensor deleter call in the same run. Bridge targets stay keyed by their stable bridge.* spelling even though compiled binaries embed them under isolated runtime namespaces.

Dynamic Zero-Copy Helpers

Lower-level dynamic APIs remain available when a typed declaration is not appropriate:
Supported surfaces:
  • Py_buffer for bytes-like objects, memoryview, NumPy, and other buffer protocol producers.
  • Arrow PyCapsules for array, schema, stream, device-array, and device-stream protocol methods.
  • DLPack capsules for NumPy, torch, and TensorFlow CPU tensor paths.
  • Array-interface protocols with owner retention and metadata validation.

Package Examples

Ordinary application code uses typed declarations. A package-local hermetic bridge is appropriate when a library workflow is dynamic internally; the Sifr boundary stays closed and typed.
biip / schwifty declarations
Dynamic library workflow behind a typed bridge
pandas / Polars Arrow declarations
PyTorch one-shot DLPack transfer
Foreign-thread Kafka callback
Cloud client workflow behind a typed bridge
Credentialed or service-backed behavior belongs in the external Python interop gate. The default local gate uses import, stub, contract, and matrix evidence.

Diagnostics

Compiler diagnostics use stable URLs and structured JSON fields: See error codes for the full catalog. Run sifr --explain SIFR-PYCALL-0001 for a stable explanation of SIFR-PYCALL-0001.