Skip to main content

Reach into any installed Python package

Sense’s own interpreter is written in Python and runs in the same process, so import python "module" is just importlib.import_module plus getattr — no FFI, no serialization boundary.
The alias defaults to the last dotted segment — import python "os.path" binds path. A module that doesn’t exist raises SenseImportError; a missing attribute or an exception raised during a call both raise SenseRuntimeError — a Sense program never sees a raw Python traceback from a foreign call, the same guarantee as everywhere else in the language.

Most Sense values already are Python values

Sense’s value model was never really “mapped onto” Python’s — for the primitives, it just is Python’s: Int is int, String is str, Array is list. So a Python function returning a list of numbers comes back as an ordinary Sense Array, usable with push/len/indexing immediately — there’s no conversion step to think about:
None / bool / int / float / str / list cross the boundary for free, both directions.

Everything else: an opaque, still-usable wrapper

A dict, a class instance, a module — anything Sense has no native representation for — comes back as a PythonValue wrapper. It isn’t “a Sense value” the way an Int is, but member access and indexing both delegate straight through to the wrapped object, so it’s still directly usable:
print() on a PythonValue shows the wrapped object’s own str(), not a placeholder — printing a pandas.DataFrame prints the table Pandas itself would print. Errors raised through a wrapped object (indexing, attribute access, calling) carry the underlying exception’s type name, since some exceptions’ messages are meaningless without it — a missing dict key raises python error indexing: KeyError: 1, not a bare 1.

The safety model: extended, not bypassed

A bare import python plus a direct call is exactly as unrestricted as a plain Sense fn — nothing new here, and nothing weaker than what already existed. A Sense function’s body has never had a capability check of its own; Python interop doesn’t open a new hole, it just means that already-documented boundary now also covers foreign calls.
The real safety story is unchanged from Actions: wrap the call in a Sense action, and it gets the full prepare → verify → commit treatment no matter what the body’s implementation happens to be:
compute_root(25) only prepares — math.sqrt doesn’t execute until .commit(), and policy: deny math.compute blocks it exactly like denying any native-Sense action’s capability. What the body is written in never changes that.

What this doesn’t do (yet)

The original design notes sketch foreign python "requests.post" as irreversible action — a declaration that classifies a function’s safety properties at the import site itself. Today that’s something you do by hand, by wrapping the call in your own action — import python itself makes no safety claim about what it imports.
An imported Python function is trusted as-is. There’s no tracing to verify a function actually behaves the way its (manual) classification claims.
No import rust "..." / import service "...". This works because Sense’s own interpreter happens to already be Python — a genuinely different host language would need a real FFI.
Only whole-module imports — no from math import sqrt-equivalent yet.
No sense-pandas/sense-numpy-style vetted wrappers. import python is the raw primitive those would be built on top of.

Continue

Actions

The prepare → verify → commit model a wrapped Python call inherits.

Roadmap

What’s built vs. still ahead for Python interop specifically.