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 preparesmath.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 actionimport 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.