PythonInterface (AxiomAI v0.1.11)

View Source

Python interpreter embedded in Elixir.

This module provides functionality to run Python code from within Elixir.

Summary

Functions

Decodes a Python object to a term.

Encodes the given term to a Python object.

Evaluates the Python code.

Convenience macro for Python code evaluation.

Installs Python and dependencies using uv package manager and initializes the interpreter.

Types

encoder()

@type encoder() :: (term(), encoder() -> PythonInterface.Object.t())

Functions

decode(object)

@spec decode(PythonInterface.Object.t()) :: term()

Decodes a Python object to a term.

Converts the following Python types to the corresponding Elixir terms:

  • NoneType
  • bool
  • int
  • float
  • str
  • bytes
  • tuple
  • list
  • dict
  • set
  • frozenset

For all other types PythonInterface.Object is returned.

Examples

iex> {result, %{}} = PythonInterface.eval("(1, True, 'hello world')", %{})
iex> PythonInterface.decode(result)
{1, true, "hello world"}

iex> {result, %{}} = PythonInterface.eval("print", %{})
iex> PythonInterface.decode(result)
#PythonInterface.Object<
  <built-in function print>
>

encode!(term, encoder \\ &PythonInterface.Encoder.encode/2)

@spec encode!(term(), encoder()) :: PythonInterface.Object.t()

Encodes the given term to a Python object.

Encoding can be extended to support custom data structures, see PythonInterface.Encoder.

Examples

iex> PythonInterface.encode!({1, true, "hello world"})
#PythonInterface.Object<
  (1, True, b'hello world')
>

eval(code, globals, opts \\ [])

@spec eval(String.t(), %{optional(String.t()) => term()}, keyword()) ::
  {PythonInterface.Object.t() | nil,
   %{optional(String.t()) => PythonInterface.Object.t()}}

Evaluates the Python code.

The globals argument is a map with global variables to be set for the evaluation. The map keys are strings, while the values can be any terms and they are automatically converted to Python objects by calling encode!/1.

The function returns the evaluation result and a map with the updated global variables. Note that the result is an object only if code ends with an expression, otherwise it is nil.

If the Python code raises an exception, PythonInterface.Error is raised and the message includes the usual Python error message with traceback.

All writes to the Python standard output are sent to caller's group leader, while writes to the standard error are sent to the :standard_error process. Reading from the standard input is not supported and raises and error.

Concurrency

The Python interpreter has a mechanism known as global interpreter lock (GIL), which prevents from multiple threads executing Python code at the same time. Consequently, calling eval/2 from multiple Elixir processes does not provide the concurrency you might expect and thus it can be a source of bottlenecks. However, this concerns regular Python code. Packages with CPU-intense functionality, such as numpy, have native implementation of many functions and invoking those releases the GIL. GIL is also released when waiting on I/O operations.

Options

  • :stdout_device - IO process to send Python stdout output to. Defaults to the caller's group leader.

  • :stderr_device - IO process to send Python stderr output to. Defaults to the global :standard_error.

Examples

iex> {result, globals} =
...>   PythonInterface.eval(
...>     """
...>     y = 10
...>     x + y
...>     """,
...>     %{"x" => 1}
...>   )
iex> result
#PythonInterface.Object<
  11
>
iex> globals["x"]
#PythonInterface.Object<
  1
>
iex> globals["y"]
#PythonInterface.Object<
  10
>

You can carry evaluation state by passing globals from one evaluation to the next:

iex> {_result, globals} = PythonInterface.eval("x = 1", %{})
iex> {result, _globals} = PythonInterface.eval("x + 1", globals)
iex> result
#PythonInterface.Object<
  2
>

Mutability

Reassigning variables will have no effect on the given globals, the returned globals will simply hold different objects:

iex> {_result, globals1} = PythonInterface.eval("x = 1", %{})
iex> {_result, globals2} = PythonInterface.eval("x = 2", globals1)
iex> globals1["x"]
#PythonInterface.Object<
  1
>
iex> globals2["x"]
#PythonInterface.Object<
  2
>

However, objects in globals are not automatically cloned, so if you explicitly mutate an object, it changes across all references:

iex> {_result, globals1} = PythonInterface.eval("x = []", %{})
iex> {_result, globals2} = PythonInterface.eval("x.append(1)", globals1)
iex> globals1["x"]
#PythonInterface.Object<
  [1]
>
iex> globals2["x"]
#PythonInterface.Object<
  [1]
>

sigil_PY(arg, list)

(macro)

Convenience macro for Python code evaluation.

This has all the characteristics of eval/2, except that globals are handled implicitly. This means that any Elixir variables referenced in the Python code will automatically get encoded and passed as globals for evaluation. Similarly, any globals assigned in the code will result in Elixir variables being defined.

Compilation

This macro evaluates Python code at compile time, so it requires the Python interpreter to be already initialized. In practice, this means that you can use this sigil in environments with dynamic evaluation, such as IEx and Livebook, but not in regular application code. In application code it is preferable to use eval/2 regardless, to make the globals management explicit.

Examples

iex> import PythonInterface
iex> x = 1
iex> ~PY"""
...> y = 10
...> x + y
...> """
#PythonInterface.Object<
  11
>
iex> x
1
iex> y
#PythonInterface.Object<
  10
>

uv_init(pyproject_toml, opts \\ [])

@spec uv_init(
  String.t(),
  keyword()
) :: :ok

Installs Python and dependencies using uv package manager and initializes the interpreter.

The interpreter is automatically initialized using the installed Python. The dependency packages are added to the module search path.

Expects a string with pyproject.toml file content, which is used to configure the project environment. The config requires project.name and project.version fields to be set. It is also a good idea to specify the Python version by setting project.requires-python.

PythonInterface.uv_init("""
[project]
name = "project"
version = "0.0.0"
requires-python = "==3.13.*"
""")

To install Python packages, set the project.dependencies field:

PythonInterface.uv_init("""
[project]
name = "project"
version = "0.0.0"
requires-python = "==3.13.*"
dependencies = [
  "numpy==2.2.2"
]
""")

For more configuration options, refer to the uv documentation.

Options

  • :force - if true, runs with empty project cache. Defaults to false.