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 runuv sync, install packages, or fall back to host-global Python.
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:
Resolution rules:
- A library without complete root authority is valid to inspect; unresolved obligations stay deferred.
- Ordinary
sifr checkuses 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 checkdiagnostics for the same snapshot.
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 usermain, 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 checkedResult:
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:
- 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.
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: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 sealedObject.kwarg[T]converts into the(str, Object)call shape.Object.get_attr,get_item,call, andcall_methodare checked method-style operations over that identity.
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:
- Package-local
--overridefiles - Selected
--stub-packagedistributions - Installed
py.typedinline sources - Package-local
--external-stubfiles - Safe runtime introspection
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 publicsifr.python operation is @blocking_io. Direct Python calls in async Sifr code are rejected unless offloaded through Sifr’s blocking offload primitive.
@python.coroutineis valid only onasync def.- Inputs and outputs use the same recursive typed conversion contract as sync declarations.
- Opaque values stay non-send.
cleanup=async_closerequires exactly one consumingaclose(own self)coroutine.- The value cannot be abandoned, closed twice, or reused after close.
- Cancelling a Sifr task cancels its exact asyncio task and waits for Python
finallycleanup first. - A terminal
CancelledErrorcaused by that request is the Sifr cancellation cause, not a catchablePythonError. - 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.
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
finallyand 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: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:bridge callables, or Self on an opaque receiver.
Selfacquisition 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.Objector opaque Python identity must useown. access=read | writeandlayout=any | c_contiguous | f_contiguousare 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:
schema=parameter(name)names a required keyword-onlyownpython.ArrowSchemaparameter.- 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
ownparameter. - That
ownparameter commits the move before the call and reconciles full, partial, or absent capsule consumption afterward.
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 affinepython.DlpackTensor[T] resources:
Self.
Device and stream:
device=cpu | cuda | anynever requests a device change.- CPU may use
stream=none. - CUDA and
anyrequirestream=parameter(name)— a required keyword-only borrowedpython.DlpackStreamfrom@python.dlpack.stream. - Runtime acquisition validates the stream family/id before the one permitted
__dlpack__call. - A CPU producer always receives
stream=None, including whendevice=anyvalidated matching CPU stream metadata.
- Sifr passes
copy=Falseandmax_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), andbool. - Sifr transfers the producer’s managed tensor without reading or mutating its payload, so
READ_ONLYis 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.
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:Py_bufferfor 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
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.