This guide walks you through using erlang_python to execute Python code from Erlang.
Add to your rebar.config:
{deps, [
{erlang_python, "1.8.0"}
]}.Or from git:
{deps, [
{erlang_python, {git, "https://github.com/benoitc/erlang-python.git", {branch, "main"}}}
]}.1> application:ensure_all_started(erlang_python).
{ok, [erlang_python]}The application starts a pool of Python worker processes that handle requests.
%% Call math.sqrt(16)
{ok, 4.0} = py:call(math, sqrt, [16]).
%% Call json.dumps with keyword arguments
{ok, Json} = py:call(json, dumps, [#{name => <<"Alice">>}], #{indent => 2}).%% Simple arithmetic
{ok, 42} = py:eval(<<"21 * 2">>).
%% Using Python built-ins
{ok, 45} = py:eval(<<"sum(range(10))">>).
%% With local variables
{ok, 100} = py:eval(<<"x * y">>, #{x => 10, y => 10}).
%% Note: Python locals aren't accessible in nested scopes (lambda/comprehensions).
%% Use default arguments to capture values:
{ok, [2, 4, 6]} = py:eval(<<"list(map(lambda x, m=multiplier: x * m, items))">>,
#{items => [1, 2, 3], multiplier => 2}).Use py:exec/1 to execute Python statements:
ok = py:exec(<<"
import random
def roll_dice(sides=6):
return random.randint(1, sides)
">>).Note: Definitions made with exec are local to the worker that executes them.
Subsequent calls may go to different workers. Use Shared State to
share data between workers, or Context Affinity to bind to a
dedicated worker.
All operations support optional timeouts:
%% 5 second timeout
{ok, Result} = py:call(mymodule, slow_func, [], #{}, 5000).
%% Timeout error
{error, timeout} = py:eval(<<"sum(range(10**9))">>, #{}, 100).For non-blocking operations:
%% Start async call
Ref = py:cast(math, factorial, [1000]).
%% Do other work...
%% Wait for result
{ok, HugeNumber} = py:await(Ref).Python generators can be streamed efficiently:
%% Stream a generator expression
{ok, [0,1,4,9,16]} = py:stream_eval(<<"(x**2 for x in range(5))">>).
%% Stream from a generator function (if defined)
{ok, Chunks} = py:stream(mymodule, generate_data, [arg1, arg2]).Python workers don't share namespace state, but you can share data via the built-in state API:
%% Store from Erlang
py:state_store(<<"config">>, #{api_key => <<"secret">>, timeout => 5000}).
%% Read from Python
ok = py:exec(<<"
from erlang import state_get
config = state_get('config')
print(config['api_key'])
">>).from erlang import state_set, state_get, state_delete, state_keys
from erlang import state_incr, state_decr
# Key-value storage
state_set('my_key', {'data': [1, 2, 3]})
value = state_get('my_key')
# Atomic counters (thread-safe)
state_incr('requests') # +1, returns new value
state_incr('requests', 10) # +10
state_decr('requests') # -1
# Management
keys = state_keys()
state_delete('my_key')py:state_store(Key, Value).
{ok, Value} = py:state_fetch(Key).
py:state_remove(Key).
Keys = py:state_keys().
%% Atomic counters
1 = py:state_incr(<<"hits">>).
11 = py:state_incr(<<"hits">>, 10).
10 = py:state_decr(<<"hits">>).Values are automatically converted between Erlang and Python:
%% Numbers
{ok, 42} = py:eval(<<"42">>). %% int -> integer
{ok, 3.14} = py:eval(<<"3.14">>). %% float -> float
%% Strings
{ok, <<"hello">>} = py:eval(<<"'hello'">>). %% str -> binary
%% Collections
{ok, [1,2,3]} = py:eval(<<"[1,2,3]">>). %% list -> list
{ok, {1,2,3}} = py:eval(<<"(1,2,3)">>). %% tuple -> tuple
{ok, #{<<"a">> := 1}} = py:eval(<<"{'a': 1}">>). %% dict -> map
%% Booleans and None
{ok, true} = py:eval(<<"True">>).
{ok, false} = py:eval(<<"False">>).
{ok, none} = py:eval(<<"None">>).By default, each call may go to a different worker. To preserve Python state across calls (variables, imports, objects), bind to a dedicated worker:
%% Bind current process to a worker
ok = py:bind(),
%% State persists across calls
ok = py:exec(<<"counter = 0">>),
ok = py:exec(<<"counter += 1">>),
{ok, 1} = py:eval(<<"counter">>),
%% Release the worker
ok = py:unbind().Or use the scoped helper for automatic cleanup:
Result = py:with_context(fun() ->
ok = py:exec(<<"x = 10">>),
py:eval(<<"x * 2">>)
end),
{ok, 20} = Result.See Context Affinity for explicit contexts and advanced usage.
Check the current execution mode:
%% See how Python is being executed
py:execution_mode().
%% => free_threaded | subinterp | multi_executor
%% Check rate limiting status
py_semaphore:max_concurrent(). %% Maximum concurrent calls
py_semaphore:current(). %% Currently executingSee Scalability for details on execution modes and performance tuning.
Forward Python logging messages to Erlang's logger:
%% Configure Python logging
ok = py:configure_logging(#{level => info}).
%% Now Python logs appear in Erlang logger
ok = py:exec(<<"
import logging
logging.info('Hello from Python!')
logging.warning('Something needs attention')
">>).Collect trace spans from Python code:
%% Enable tracing
ok = py:enable_tracing().
%% Run traced Python code
ok = py:exec(<<"
import erlang
with erlang.Span('my-operation', key='value'):
pass # your code here
">>).
%% Retrieve spans
{ok, Spans} = py:get_traces().
%% Clean up
ok = py:clear_traces().
ok = py:disable_tracing().See Logging and Tracing for details on span events, decorators, and error handling.
erlang_python works seamlessly with Elixir. The :py module can be called directly:
# Start the application
{:ok, _} = Application.ensure_all_started(:erlang_python)
# Call Python functions
{:ok, 4.0} = :py.call(:math, :sqrt, [16])
# Evaluate expressions
{:ok, result} = :py.eval("2 + 2")
# With variables
{:ok, 100} = :py.eval("x * y", %{x: 10, y: 10})
# Call with keyword arguments
{:ok, json} = :py.call(:json, :dumps, [%{name: "Elixir"}], %{indent: 2})# Register an Elixir function
:py.register_function(:factorial, fn [n] ->
Enum.reduce(1..n, 1, &*/2)
end)
# Call from Python
{:ok, 3628800} = :py.eval("__import__('erlang').call('factorial', 10)")
# Cleanup
:py.unregister_function(:factorial)# Register parallel map using BEAM processes
:py.register_function(:parallel_map, fn [func_name, items] ->
parent = self()
refs = Enum.map(items, fn item ->
ref = make_ref()
spawn(fn ->
result = apply_function(func_name, item)
send(parent, {ref, result})
end)
ref
end)
Enum.map(refs, fn ref ->
receive do
{^ref, result} -> result
after
5000 -> {:error, :timeout}
end
end)
end)A complete working example is available:
elixir --erl "-pa _build/default/lib/erlang_python/ebin" examples/elixir_example.exsThis demonstrates basic calls, data conversion, callbacks, parallel processing (10x speedup), and AI integration.
For async Python code, use the erlang module which provides an Erlang-backed asyncio event loop for better performance:
import erlang
import asyncio
async def my_handler():
# Uses Erlang's erlang:send_after/3 - no Python event loop overhead
await asyncio.sleep(0.1) # 100ms
return "done"
# Run a coroutine with the Erlang event loop
result = erlang.run(my_handler())
# Standard asyncio functions work seamlessly
async def main():
results = await asyncio.gather(task1(), task2(), task3())
erlang.run(main())This is especially useful in ASGI handlers where sleep operations are common. See Asyncio for the full API reference.
When Python runs inside the Erlang VM, certain operations are blocked for safety:
- Subprocess operations blocked -
subprocess.Popen,os.fork(),os.system(), etc. would corrupt the Erlang VM - Signal handling not supported - Signal handling should be done at the Erlang level
If you need to run external commands, use Erlang ports (open_port/2) instead:
%% From Erlang - run a shell command
Port = open_port({spawn, "ls -la"}, [exit_status, binary]),
receive
{Port, {data, Data}} -> Data;
{Port, {exit_status, 0}} -> ok
end.See Security for details on blocked operations and recommended alternatives.
- See Type Conversion for detailed type mapping
- See Context Affinity for preserving Python state
- See Streaming for working with generators
- See Memory Management for GC and debugging
- See Scalability for parallelism and performance
- See Logging and Tracing for Python logging and distributed tracing
- See AI Integration for ML/AI examples
- See Asyncio Event Loop for the Erlang-native asyncio implementation with TCP and UDP support
- See Reactor for FD-based protocol handling
- See Security for sandbox and blocked operations
- See Web Frameworks for ASGI/WSGI integration