Erlang_python - embed Python in Erlang/Elixir

erlang_python embeds the Python interpreter into the BEAM VM using dirty NIFs. It allows calling Python functions, evaluating expressions, and streaming from generators without blocking Erlang schedulers. While it works with any Python code, it was designed with ML/AI workloads in mind, ie. embeddings, inference, data processing.

Basic usage:

{ok, 4.0} = py:call(math, sqrt, [16]).
{ok, Json} = py:call(json, dumps, [#{foo => bar}]).
{ok, 45} = py:eval(<<"sum(range(10))">>).

From Elixir:

{:ok, 4.0} = :py.call(:math, :sqrt, [16])
{:ok, json} = :py.call(:json, :dumps, [%{foo: "bar"}])
{:ok, 45} = :py.eval("sum(range(10))")

Async, streaming, and parallel execution:

% async calls
Ref = py:call_async(slow_module, compute, [Data]),
{ok, Result} = py:await(Ref).

% iterate over generators - useful for LLM token streaming
{ok, Chunks} = py:stream(mymodule, generate, []).

% native asyncio support
Ref = py:async_call(aiohttp, get, [Url]),
{ok, Response} = py:async_await(Ref).

% parallel execution using sub-interpreters (Python 3.12+)
{ok, Results} = py:parallel([
    {numpy, dot, [A, B]},
    {numpy, dot, [C, D]}
]).

The library handles GIL contention depending on your Python version:

  • Python < 3.12: multiple executor threads sharing the GIL
  • Python 3.12+: sub-interpreters, each with its own GIL
  • Python 3.13t: free-threaded build, no GIL

Erlang/Elixir functions can be registered and called from Python:

py:register_function(my_func, fun(Args) -> do_stuff(Args) end).

import erlang
result = erlang.call('my_func', arg1, arg2)

Virtual environment support is included for dependency isolation.

Requirements: OTP 27+, Python 3.11+, CMake 3.18+
Platforms: Linux, macOS, FreeBSD

Just released erlang_python 1.0.0.

Hex: erlang_python | Hex
Docs: erlang_python v1.0.0 — Documentation
Repo:

Feedback welcome.

10 Likes

Quick update - v1.2.0 is out.

Context affinity - py:bind() pins your process to a specific worker. State persists across calls, so you can load an ML model once and reuse it. Cleanup via process monitors when the process dies.

Python thread support - threads spawned by Python (threading.Thread, ThreadPoolExecutor) can call erlang.call() without deadlocking. Each thread gets a dedicated channel back.

Reentrant callbacks - Python→Erlang→Python chains work now. Uses an exception-based suspension mechanism to avoid blocking the worker pool.

Shared state (from 1.1.0) - ETS-backed storage accessible from both sides. py:state_store/fetch in Erlang, from erlang import state_get, state_set in Python.

Changelog: Releases · benoitc/erlang-python · GitHub

2 Likes