Python as an embedded language for Elixir, powered by RustPython through Rustler.
No Python installation is involved: the interpreter — and, by default, the standard library — is compiled into the NIF.
{:ok, session} = Rupyex.open()
{:ok, 3} = Rupyex.eval(session, "1 + 2")
{:ok, nil} = Rupyex.eval(session, "def double(x): return x * 2")
{:ok, 42} = Rupyex.call(session, "double", [21])
{:ok, %{"total" => 6}} =
Rupyex.eval(session, "{'total': sum(xs)}", bind: %{"xs" => [1, 2, 3]})Sessions hold state
A session is a live namespace: names bound by one call are visible to the
next, exactly like a REPL. Sessions are cheap but not free (each owns an OS
thread), so keep one around for as long as the state matters and close it
when you are done. For a single throwaway snippet, use eval_once/2.
Values
| Python | Elixir |
|---|---|
None | nil |
True / False | true / false |
int | integer (of any size) |
float | float |
str | binary |
bytes, bytearray | Rupyex.Bytes |
list | list |
tuple | tuple |
dict | map |
set, frozenset | Rupyex.Set |
nan, inf, -inf | :nan, :infinity, :neg_infinity |
| anything else | Rupyex.Object (class name and repr) |
Going the other way, atoms other than nil/true/false become strings,
and a binary becomes a str when it is valid UTF-8 and bytes when it is
not. Rupyex.Object values cannot be passed back in — the object never left
the interpreter, so refer to it by name instead.
Values are copied rather than shared, and anything that cannot cross fails
with a kind: :conversion error. See the
data exchange guide for the whole story.
Timeouts
Every call takes a :timeout (default 5s). Python code that overruns it is
interrupted with a KeyboardInterrupt at its next safe point, so a runaway
loop cannot pin a scheduler or leak a thread. Python never runs on a BEAM
scheduler in the first place: each session owns a thread, and calls are
ordinary message round-trips.
Output
print output is captured per call. eval/3 discards it; run/3 returns it
in a Rupyex.Result, and an error carries whatever was printed before it was
raised. Pass capture_output: false to open/1 to let Python write to the
BEAM's own stdout instead.
Summary
Functions
Call a Python callable by name and return its value.
Same as call/4, but returns the value directly and raises on failure.
Stop a session.
Unbind a name from the session namespace.
Run Python source and return its value.
Same as eval/3, but returns the value directly and raises Rupyex.Error
on failure.
Run a snippet in a fresh session and throw the session away.
Read a name from the session namespace.
Abort whatever the session is running right now.
Start a session. See Rupyex.Session.open/1 for the options.
Start a session, raising on failure.
Bind a name in the session namespace.
Throw the namespace away and start from a clean one.
Run Python source and return its value along with everything it printed.
Whether this build embeds the Python standard library.
The names bound in the session namespace, excluding dunders.
Types
@type option() :: {:timeout, timeout()} | {:bind, map() | keyword()} | {:mode, :block | :eval | :exec} | {:file, String.t()}
Options accepted by eval/3, run/3 and call/4:
:timeout— override the session default for this call:bind— names to bind in the session namespace before running the code, as a map or keyword list (eval/3andrun/3only):mode— how to compile the source (eval/3andrun/3only)::block(default) — run the statements, return the value of the last one:eval— a single expression:exec— statements only, always returnsnil
:file— the file name shown in tracebacks (default"<rupyex>")
@type session() :: Rupyex.Session.t()
@type value() :: term()
Functions
@spec call(session(), String.t(), [value()], keyword() | map()) :: {:ok, value()} | {:error, Rupyex.Error.t()}
Call a Python callable by name and return its value.
The name is resolved against the session namespace first, then the builtins, and may walk attributes:
{:ok, 3} = Rupyex.call(session, "len", ["abc"])
{:ok, "[1, 2]"} = Rupyex.call(session, "json.dumps", [[1, 2]])Keyword arguments go in a separate list, since Python distinguishes them from positional ones:
{:ok, "{"a": 1 }"} =
Rupyex.call(session, "json.dumps", [%{"a" => 1}], indent: 2):timeout in that list is read as a call option rather than a keyword
argument; pass a map to send it to Python instead.
Same as call/4, but returns the value directly and raises on failure.
@spec close(session()) :: :ok
Stop a session.
@spec delete(session(), String.t(), [option()]) :: :ok | {:error, Rupyex.Error.t()}
Unbind a name from the session namespace.
@spec eval(session(), String.t(), [option()]) :: {:ok, value()} | {:error, Rupyex.Error.t()}
Run Python source and return its value.
{:ok, 3} = Rupyex.eval(session, "1 + 2")
{:ok, 6} = Rupyex.eval(session, "x = 1 + 2\nx * 2")
{:ok, 5} = Rupyex.eval(session, "a + b", bind: [a: 2, b: 3])Anything the code prints is discarded; use run/3 to keep it.
Same as eval/3, but returns the value directly and raises Rupyex.Error
on failure.
@spec eval_once(String.t(), [option() | Rupyex.Session.open_option()]) :: {:ok, value()} | {:error, Rupyex.Error.t()}
Run a snippet in a fresh session and throw the session away.
Convenient for one-off evaluation; if you are going to run more than one
snippet, open/1 a session and reuse it — starting an interpreter costs far
more than a call into one.
{:ok, 4} = Rupyex.eval_once("2 ** 2")
@spec get(session(), String.t(), [option()]) :: {:ok, value()} | {:error, Rupyex.Error.t()}
Read a name from the session namespace.
:ok = Rupyex.put(session, "x", 1)
{:ok, 1} = Rupyex.get(session, "x")
@spec interrupt(session()) :: :ok
Abort whatever the session is running right now.
@spec open([Rupyex.Session.open_option()]) :: {:ok, session()} | {:error, Rupyex.Error.t()}
Start a session. See Rupyex.Session.open/1 for the options.
@spec open!([Rupyex.Session.open_option()]) :: session()
Start a session, raising on failure.
@spec put(session(), String.t(), value(), [option()]) :: :ok | {:error, Rupyex.Error.t()}
Bind a name in the session namespace.
:ok = Rupyex.put(session, "config", %{"retries" => 3})
{:ok, 3} = Rupyex.eval(session, "config['retries']")
@spec reset(session(), [option()]) :: :ok | {:error, Rupyex.Error.t()}
Throw the namespace away and start from a clean one.
Imported modules stay loaded in the interpreter; only the names bound in the session go.
@spec run(session(), String.t(), [option()]) :: {:ok, Rupyex.Result.t()} | {:error, Rupyex.Error.t()}
Run Python source and return its value along with everything it printed.
{:ok, %Rupyex.Result{value: nil, stdout: "hi\n"}} =
Rupyex.run(session, "print('hi')")
@spec stdlib_available?() :: boolean()
Whether this build embeds the Python standard library.
@spec vars(session(), [option()]) :: {:ok, [String.t()]} | {:error, Rupyex.Error.t()}
The names bound in the session namespace, excluding dunders.