<!-- Sema documentation — Python Interop
     Sema is a new AI-native, neurosymbolic programming language most models
     have not been trained on. Full corpus: https://sema.49.12.246.95.sslip.io/llms-full.txt
     Install language support: https://sema.49.12.246.95.sslip.io/install-skill/ -->

# Python Interop

> Reuse the whole Python ecosystem from Sema — live objects and classes via the persistent worker, plus governed bridges and verified ports.

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`](/reference/examples-api/hybrid-interop/) example, which wraps
Python, TypeScript, and C outputs in one validated Sema review.

## 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

`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:

```sema
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.

:::note
The worker's protocol owns stdout, so a library that `print()`s can't corrupt the
channel — a robustness property, not something you configure.
:::

The interpreter is resolved as `config python.bin` → `$SEMA_PYTHON` → `python3`.
Point it at the environment [`sema add`](/guides/packaging/) builds so the
packages you installed are importable.

## 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`](/reference/examples-api/crisis-logistics/) example binds a
routing library this way:

```sema
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.

## 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:

```sema
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.

## 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:

```sema
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:

```sema
normalized_title = title_glue.normalize_title(doc.title)
features = text_features.extract_text_features(doc)
```

:::caution[Foreign returns are untrusted]
A value crossing a bridge is born `untrusted` and only becomes usable once its
blocking contracts pass. An inline foreign block that requests a forbidden import
or effect is rejected at the bridge policy boundary; exceptions cross back as
typed `ForeignError` values.
:::

## Overriding a model backend with Python

Interop and the capability system meet in
[`@provides`](/guides/packaging/#custom-capability-providers): a Sema function
tagged `@provides("embed")` can wrap a Python model and become *the* embedder that
`~=` and `semantic.*` route through — no Rust:

```sema
import python

@provides("embed")
def my_embed(text: str) -> list[f64] !{proc.run}:
    return python.call("sentence_transformers_helper", "encode", [text])
```

See [Packaging & Providers](/guides/packaging/) for the full provider surface.

## Run and verify

From the `sema/` directory:

```bash
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.

## Variations

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

## See also

- [hybrid-interop example (generated)](/reference/examples-api/hybrid-interop/)
- [Packaging & Providers](/guides/packaging/) · [Traits, Enums & Generics](/language/traits-enums-generics/)
- [Error Handling](/language/error-handling/)
