Python Interop
Sema doesn’t ask you to abandon NumPy, PyTorch, or the rest of PyPI. There are two ways to bring Python code into a Sema program, chosen by risk profile:
- Bind an ecosystem library — call it live, classes and objects included,
through a persistent worker. This is
import pythonandnative import. - Absorb self-contained algorithmic code — translate a small pure function
into Sema, or wrap trusted foreign code behind a validated membrane. This is
ported defandbridge.
This guide covers both, anchored on the
hybrid-interop example, which wraps
Python, TypeScript, and C outputs in one validated Sema review.
The pieces
Section titled “The pieces”| Goal | Construct | Section |
|---|---|---|
| Call a live Python library | import python | §“Live Python objects” |
| Bind a library at the language level | native import | §“native import” |
| Translate pure code into Sema | ported def … from | §“Ported code” |
| Wrap trusted foreign code, validated | bridge | §“Bridges” |
Live Python objects
Section titled “Live Python objects”import python gives you a single persistent Python worker — one warm
process, started lazily, reused for every call. Anything not JSON-serializable (a
NumPy array, a class instance, a module) comes back as an object handle whose
attributes and methods dispatch back into the worker:
import pythonnp = python.import("numpy")a = np.array([1.0, 2.0, 3.0, 4.0]) # a live NumPy array (handle)a.sum() # -> 10 (native method call)a.mean() # -> 2.5python.attr(a, "shape") # -> [4]python.call("numpy.linalg", "det", [np.array([[1.0,2.0],[3.0,4.0]])]) # -> -2JSON-serializable results come back as native Sema values (numbers, lists,
dicts); numpy/torch scalars are coerced to numbers; everything else stays a
handle so its methods keep working. obj.method(...) and obj.attr work
natively via the handle; python.method(obj, name, args) and
python.attr(obj, name) are the explicit forms.
The interpreter is resolved as config python.bin → $SEMA_PYTHON → python3.
Point it at the environment sema add builds so the
packages you installed are importable.
native import
Section titled “native import”native import binds a library at the language level — the module becomes a
first-class value with a name, never translated. The
crisis-logistics example binds a
routing library this way:
native import geos.routing as routingThe tier prefix in the path selects the host and isolation tier —
native import numpy as np is the embedded, trusted fast path;
native import python.isolated.pdf as pdf runs in an isolated worker. Foreign
calls carry a declared effect row and their returns are born untrusted until
your contracts pass.
Ported code
Section titled “Ported code”For a small, self-contained algorithm — no ecosystem dependencies — you can
translate the Python into Sema with ported def … from. The source stays the
oracle: the toolchain translates under type-constrained decoding, then gates the
result with differential against source (the port must agree with the original)
plus your contracts. crisis-logistics ports a distance function:
ported def haversine_km(a_lat: f64, a_lon: f64, b_lat: f64, b_lon: f64) -> f64 from "vendor/haversine.py": ensure result >= 0.0 differential against sourceOnce admitted, ported code is ordinary Sema — full verification, policies, and
monitors apply, and re-translation happens only when the source hash changes.
Translating ecosystem-dependent code (anything NumPy-class) is a compile error
that directs you to native instead — that is what bindings are for.
Bridges
Section titled “Bridges”A bridge is the authoring membrane for trusted foreign functions. Only
expose def signatures are callable from Sema, each carrying ordinary Sema types,
effects, descriptors, and contracts — foreign return values are re-validated
at the membrane and are untrusted until the blocking contracts pass. The
hybrid-interop example wraps Python, TypeScript, and C behind one file:
bridge python.inline text_features from "foreign/python/text_features.py": deps "python>=3.14"
expose: def extract_text_features(doc: DocumentInput) -> TextFeatures !{ffi.call}: sem "Call trusted Python text-feature code and revalidate the result" require len(doc.body) > 0 ensure result.token_count >= 1 check semantics("features are supported by the document text", doc, result, alpha=0.02)
bridge python.isolated title_glue: expose def normalize_title(raw: str) -> str !{ffi.call}: sem "Normalize title whitespace in an isolated Python worker" ensure len(result) > 0 begin python def normalize_title(raw): return " ".join(raw.split()) end pythonpython.inlineis a fast, in-process worker (best-effort confinement);python.isolatedis capability-exact at the process boundary — choose by trust.expose:lists several exposed signatures inside one boundary without repeatingexpose def; each still gets its own type, effect row, contracts, and blame label.- An inline
begin python … end pythonblock keeps foreign code delimited, so formatters and stack traces don’t guess where host code ends.
Calling a bridged function is then plain Sema:
normalized_title = title_glue.normalize_title(doc.title)features = text_features.extract_text_features(doc)Overriding a model backend with Python
Section titled “Overriding a model backend with Python”Interop and the capability system meet in
@provides: a Sema function
tagged @provides("embed") can wrap a Python model and become the embedder that
~= and semantic.* route through — no Rust:
import python
@provides("embed")def my_embed(text: str) -> list[f64] !{proc.run}: return python.call("sentence_transformers_helper", "encode", [text])See Packaging & Providers for the full provider surface.
Run and verify
Section titled “Run and verify”From the sema/ directory:
sema check examples/hybrid-interopSEMA_STRICT=1 sema run examples/hybrid-interopsema assure examples/hybrid-interop --grade goldhybrid-interop runs at assure gold, and its bridge outputs are watched by a
monitor for drift — foreign code is governed like everything else.
Variations
Section titled “Variations”- Batch through the worker. One
python.callcan hand a whole list to a vectorized library and get a list back — keep the crossings coarse-grained. - Isolate untrusted code. Prefer
python.isolated/bridge python.isolatedfor anything you don’t fully trust; the confinement is at the process boundary. - Port, don’t bind, pure helpers. A dependency-free algorithm is better as a
ported def(verified, native, effect-typed) than a live bridge call.