Skip to content

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:

  1. Bind an ecosystem library — call it live, classes and objects included, through a persistent worker. This is import python and native import.
  2. Absorb self-contained algorithmic code — translate a small pure function into Sema, or wrap trusted foreign code behind a validated membrane. This is ported def and bridge.

This guide covers both, anchored on the hybrid-interop example, which wraps Python, TypeScript, and C outputs in one validated Sema review.

GoalConstructSection
Call a live Python libraryimport python§“Live Python objects”
Bind a library at the language levelnative import§“native import”
Translate pure code into Semaported def … from§“Ported code”
Wrap trusted foreign code, validatedbridge§“Bridges”

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 python
np = 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.5
python.attr(a, "shape") # -> [4]
python.call("numpy.linalg", "det", [np.array([[1.0,2.0],[3.0,4.0]])]) # -> -2

JSON-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_PYTHONpython3. Point it at the environment sema add builds so the packages you installed are importable.

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 routing

The 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.

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 source

Once 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.

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 python
  • python.inline is a fast, in-process worker (best-effort confinement); python.isolated is capability-exact at the process boundary — choose by trust.
  • expose: lists several exposed signatures inside one boundary without repeating expose def; each still gets its own type, effect row, contracts, and blame label.
  • An inline begin python … end python block 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)

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.

From the sema/ directory:

Terminal window
sema check examples/hybrid-interop
SEMA_STRICT=1 sema run examples/hybrid-interop
sema assure examples/hybrid-interop --grade gold

hybrid-interop runs at assure gold, and its bridge outputs are watched by a monitor for drift — foreign code is governed like everything else.

  • Batch through the worker. One python.call can 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.isolated for 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.