The Engineering Reality of Monitoring Real-Time Conversations
Explore the technical challenges of building real-time conversation monitoring systems, from handling massive concurrency to integrating AI for instant analysis.
Read more →The Elixir and Python ecosystems each have unique strengths: Elixir excels at building concurrent, fault-tolerant systems, while Python dominates in machine learning, data science, and scientific computing. But what if you could combine both in the same application without the complexity of microservices or external APIs?
Enter Pythonx – a library that embeds a Python interpreter directly into your Elixir application, enabling you to evaluate Python code, call Python libraries, and seamlessly convert data between both languages.
In this post, we’ll explore what Pythonx is, how it works, practical use cases, and when it makes sense to use this powerful interoperability tool.
Before Pythonx, integrating Python with Elixir typically meant choosing between several approaches:
While these approaches work, they add operational complexity, latency, and require managing separate processes or services. For many use cases, especially in development, prototyping, or when using Livebook for data science workflows, a simpler solution is desirable.
Pythonx is an open-source library maintained by the Livebook team that embeds a Python interpreter directly into your Elixir application using Erlang NIFs (Native Implemented Functions). This means Python runs in the same OS process as your BEAM application, enabling fast, efficient communication between Elixir and Python code.
uv for fast, reliable Python dependency management~PY sigil for interactive use in IEx and LivebookUnder the hood, Pythonx uses NIFs to link against CPython’s shared library and embed the interpreter directly in the BEAM. This architecture provides:
However, this design also comes with important trade-offs we’ll discuss later.
Mix.install([
{:pythonx, "~> 0.4.0"}
])
Add to your mix.exs:
def deps do
[
{:pythonx, "~> 0.4.0"}
]
end
Then configure in config/config.exs:
config :pythonx,
python_version: "3.12",
python_packages: [
"numpy==1.26.0",
"pandas==2.1.0",
"scikit-learn==1.3.0"
]
This configuration downloads and bundles Python dependencies at compile time, making them part of your Elixir release.
# Evaluate Python code
Pythonx.eval("2 + 2")
# => 4
# Use variables
Pythonx.eval("x + y", %{"x" => 10, "y" => 5})
# => 15
# Work with strings
Pythonx.eval("name.upper()", %{"name" => "elixir"})
# => "ELIXIR"
Pythonx automatically converts between Python and Elixir data types:
# Lists
Pythonx.eval("[1, 2, 3]")
# => [1, 2, 3]
# Dictionaries to maps
Pythonx.eval("{'name': 'Alice', 'age': 30}")
# => %{"name" => "Alice", "age" => 30}
# Nested structures
Pythonx.eval("{'users': [{'id': 1, 'name': 'Bob'}, {'id': 2, 'name': 'Carol'}]}")
# => %{"users" => [%{"id" => 1, "name" => "Bob"}, %{"id" => 2, "name" => "Carol"}]}
# NumPy operations
Pythonx.eval("""
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
arr.mean()
""")
# => 3.0
# Pandas DataFrames
result = Pythonx.eval("""
import pandas as pd
df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
df.sum().to_dict()
""")
# => %{"A" => 6, "B" => 15}
One of the most compelling use cases is integrating existing Python ML models into Elixir applications:
defmodule MLPredictor do
def load_model do
Pythonx.eval("""
import joblib
model = joblib.load('model.pkl')
""")
end
def predict(features) when is_list(features) do
Pythonx.eval(
"""
import numpy as np
X = np.array(features).reshape(1, -1)
prediction = model.predict(X)[0]
confidence = model.predict_proba(X)[0].max()
{'prediction': int(prediction), 'confidence': float(confidence)}
""",
%{"features" => features}
)
end
end
# Usage
MLPredictor.load_model()
MLPredictor.predict([5.1, 3.5, 1.4, 0.2])
# => %{"prediction" => 0, "confidence" => 0.95}
Pythonx shines in Livebook notebooks where you can mix Elixir’s data processing with Python’s visualization libraries:
# In a Livebook cell
data = MyApp.Repo.all(MyApp.Sale)
|> Enum.map(fn sale ->
%{date: sale.date, amount: sale.amount}
end)
# Convert to Python and visualize
~PY"""
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame($data)
df.plot(x='date', y='amount', kind='line')
plt.savefig('sales.png')
"""
Leverage Python’s scientific libraries for complex calculations:
defmodule ScientificCompute do
def solve_linear_system(a_matrix, b_vector) do
Pythonx.eval(
"""
import numpy as np
A = np.array(a_matrix)
b = np.array(b_vector)
x = np.linalg.solve(A, b)
x.tolist()
""",
%{"a_matrix" => a_matrix, "b_vector" => b_vector}
)
end
def fft_analysis(signal) do
Pythonx.eval(
"""
import numpy as np
from scipy.fft import fft
signal_array = np.array(signal)
fft_result = fft(signal_array)
{
'magnitude': np.abs(fft_result).tolist(),
'phase': np.angle(fft_result).tolist()
}
""",
%{"signal" => signal}
)
end
end
Use Python’s NLP libraries like spaCy or NLTK:
defmodule TextAnalyzer do
def init do
Pythonx.eval("""
import spacy
nlp = spacy.load('en_core_web_sm')
""")
end
def extract_entities(text) do
Pythonx.eval(
"""
doc = nlp(text)
entities = [
{'text': ent.text, 'label': ent.label_}
for ent in doc.ents
]
entities
""",
%{"text" => text}
)
end
def sentiment_analysis(text) do
Pythonx.eval(
"""
from textblob import TextBlob
blob = TextBlob(text)
{
'polarity': blob.sentiment.polarity,
'subjectivity': blob.sentiment.subjectivity
}
""",
%{"text" => text}
)
end
end
# Usage
TextAnalyzer.init()
TextAnalyzer.extract_entities("Apple Inc. was founded in Cupertino by Steve Jobs.")
# => [
# %{"text" => "Apple Inc.", "label" => "ORG"},
# %{"text" => "Cupertino", "label" => "GPE"},
# %{"text" => "Steve Jobs", "label" => "PERSON"}
# ]
Integrate Python’s powerful image processing libraries:
defmodule ImageProcessor do
def resize_image(image_path, width, height) do
Pythonx.eval(
"""
from PIL import Image
import io
import base64
img = Image.open(image_path)
img_resized = img.resize((width, height))
buffer = io.BytesIO()
img_resized.save(buffer, format='PNG')
buffer.seek(0)
base64.b64encode(buffer.read()).decode()
""",
%{"image_path" => image_path, "width" => width, "height" => height}
)
end
def detect_faces(image_path) do
Pythonx.eval(
"""
import cv2
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
face_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
)
faces = face_cascade.detectMultiScale(gray, 1.1, 4)
[{'x': int(x), 'y': int(y), 'w': int(w), 'h': int(h)} for (x, y, w, h) in faces]
""",
%{"image_path" => image_path}
)
end
end
The most critical limitation of Pythonx is Python’s Global Interpreter Lock (GIL). The GIL prevents multiple threads from executing Python code simultaneously, which has important implications:
Impact on Concurrency:
# This won't give you the concurrency you expect!
tasks = for i <- 1..10 do
Task.async(fn ->
Pythonx.eval("expensive_computation(#{i})")
end)
end
Task.await_many(tasks)
Even though you’re spawning 10 Elixir processes, they’ll be serialized when executing Python code due to the GIL.
Solutions:
Use a Single Process: Route all Python calls through one GenServer
defmodule PythonxServer do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
def eval(code, vars \\ %{}) do
GenServer.call(__MODULE__, {:eval, code, vars})
end
def handle_call({:eval, code, vars}, _from, state) do
result = Pythonx.eval(code, vars)
{:reply, result, state}
end
end
Leverage Native Libraries: Packages like NumPy and Pandas release the GIL for their computationally intensive operations
# These operations release the GIL and can run concurrently
Pythonx.eval("numpy.dot(large_matrix_a, large_matrix_b)")
Use Ports for True Parallelism: For CPU-bound Python work that needs true parallelism:
# Spawn multiple Python processes for parallel work
tasks = for i <- 1..10 do
Task.async(fn ->
System.cmd("python", ["script.py", to_string(i)])
end)
end
Python objects live in the same process as your BEAM application:
Python exceptions need proper handling:
defmodule SafePythonx do
def safe_eval(code, vars \\ %{}) do
try do
{:ok, Pythonx.eval(code, vars)}
rescue
e -> {:error, Exception.message(e)}
end
end
end
case SafePythonx.safe_eval("1 / 0") do
{:ok, result} -> result
{:error, msg} -> Logger.error("Python error: #{msg}")
end
# Bad: Multiple calls
def process_items(items) do
Enum.map(items, fn item ->
Pythonx.eval("process(item)", %{"item" => item})
end)
end
# Good: Single batch call
def process_items(items) do
Pythonx.eval(
"[process(item) for item in items]",
%{"items" => items}
)
end
defmodule MLModel do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
def init(_) do
# Load model once at startup
Pythonx.eval("""
import tensorflow as tf
model = tf.keras.models.load_model('model.h5')
""")
{:ok, nil}
end
def predict(input) do
GenServer.call(__MODULE__, {:predict, input})
end
def handle_call({:predict, input}, _from, state) do
result = Pythonx.eval("model.predict(input)", %{"input" => input})
{:reply, result, state}
end
end
@spec analyze_sentiment(String.t()) :: {:ok, float()} | {:error, String.t()}
def analyze_sentiment(text) when is_binary(text) do
try do
score = Pythonx.eval(
"TextBlob(text).sentiment.polarity",
%{"text" => text}
)
{:ok, score}
rescue
e -> {:error, Exception.message(e)}
end
end
✅ Developing in Livebook and need Python libraries ✅ Prototyping ML integration before building production solutions ✅ Working with Python libraries that have no Elixir equivalent ✅ Calls are infrequent or can be batched ✅ Using Python libraries that release the GIL (NumPy, etc.) ✅ Simplicity matters more than maximum performance
❌ You need true parallel execution of Python code ❌ Building high-throughput production systems ❌ Python operations are frequent and distributed across many processes ❌ You can use pure Elixir alternatives (Nx, Axon, Explorer) ❌ Your Python code is long-running or stateful
1. Nx/Axon for ML: Pure Elixir machine learning
# Native Elixir ML - no Python needed
model = Axon.input("input", shape: {nil, 784})
|> Axon.dense(128, activation: :relu)
|> Axon.dense(10, activation: :softmax)
2. Ports for Parallelism: True concurrent Python execution
Port.open({:spawn, "python ml_worker.py"}, [:binary])
3. HTTP APIs: Separate Python services
HTTPoison.post("http://python-service/predict", Jason.encode!(data))
4. Explorer for DataFrames: Elixir’s answer to Pandas
df = Explorer.DataFrame.new(%{a: [1, 2, 3], b: [4, 5, 6]})
Explorer.DataFrame.mutate(df, c: a + b)
Here’s how you might architect a production system using Pythonx strategically:
defmodule MyApp.ML.Pipeline do
# Use Elixir for orchestration, concurrency, and most processing
def process_batch(records) do
records
|> Stream.chunk_every(100)
|> Task.async_stream(&preprocess/1, max_concurrency: 10)
|> Stream.map(fn {:ok, batch} -> batch end)
# Single Python call for the ML inference step
|> Enum.map(&predict_with_python/1)
|> Stream.map(&postprocess/1)
|> Enum.to_list()
end
# Elixir handles data transformation
defp preprocess(batch) do
Enum.map(batch, fn record ->
%{
features: extract_features(record),
metadata: record.metadata
}
end)
end
# Python handles ML inference (called through single process)
defp predict_with_python(batch) do
PythonxServer.predict(batch)
end
# Elixir handles result processing
defp postprocess(predictions) do
Enum.map(predictions, &format_prediction/1)
end
end
The ecosystem continues to evolve:
The trend is toward more native Elixir solutions while maintaining pragmatic Python integration where it makes sense.
Pythonx represents a pragmatic approach to Python-Elixir integration, particularly valuable for:
However, it’s not a silver bullet. The GIL limitations mean it’s not suitable for high-concurrency production systems that need to execute Python code in parallel. For those scenarios, consider ports, HTTP APIs, or increasingly mature native Elixir solutions.
The key is choosing the right tool for your specific use case. Pythonx opens doors to Python’s ecosystem while letting you stay within your Elixir application, making it an invaluable tool when used appropriately.
Ready to try Pythonx? Here’s your path forward:
~PY sigilAt AsyncSquad Labs, we specialize in building scalable Elixir applications and can help you navigate the Python-Elixir integration landscape. Whether you need to leverage existing Python ML models, migrate to native Elixir solutions, or architect a hybrid system, we can guide you to the right solution.
Contact us to discuss your integration needs and learn how we can help you combine the best of both ecosystems.