kzemek
I’ve recently released v0.2.0 of my Python interop library, Snex.
This version rolls up all work-in-progress improvements that have been driven by half a year of use in a production Elixir system. Finally had some time to fully clean them up, and release a version that I’m proud of.
Snex is a library for interfacing with Python code in a tightly integrated way. The code is run by sidecar Python interpreters, managed by an Elixir runtime, and communicating through a light Snex Python runtime.
A big design goal of the library is to achieve devx similar to pythonx, while avoiding GIL-related issues and maintenance burdens of C-level integration - where both the BEAM and CPython APIs are constantly moving targets.
Highlights
-
Run multiple Python interpreters in separate OS processes, preventing GIL issues from affecting your Elixir application.
-
Leverages
uvto manage Python versions and dependencies, embedding them into your application’s release for consistent deployments. -
A powerful and efficient interface with explicit control over data passing between Elixir and Python processes.
-
Supports custom Python environments,
asynciocode, and integration with external Python projects. -
Built on stable foundations, so future versions of Python or Elixir are unlikely to require Snex updates to use - they should work day one!
Quick example
defmodule SnexTest.NumpyInterpreter do
use Snex.Interpreter,
pyproject_toml: """
[project]
name = "my-numpy-project"
version = "0.0.0"
requires-python = ">=3.10,<3.15"
dependencies = ["numpy>=2"]
"""
end
{:ok, inp} = SnexTest.NumpyInterpreter.start_link()
{:ok, env} = Snex.make_env(inp)
{:ok, 6.0} =
Snex.pyeval(env,
"""
import numpy as np
matrix = np.fromfunction(lambda i, j: (-1) ** (i + j), (s, s), dtype=int)
""",
%{"s" => 6},
returning: "np.linalg.norm(matrix)")
Links
GitHub: GitHub - kzemek/snex:
Easy and efficient Python interop for Elixir
Hex: snex | Hex
HexDocs: snex v0.2.0 — Documentation
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #blog-post
- #ai
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 7- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
Asd
Hi, very impressive library. I’ve read the code and it looks really great, however I have these questions and notes.
Snex.Interpreter’s state. I don’t know about good workaroundinit_python_portfunction. I’d suggest to add a timeout as a failsafepyevalcalls, right? But it is possible to setup some env, execute a command in it setting a variable and this variable will be set in this env (with the same ID, I mean), right?pyevalwith async code and dies. It would make sense for interpreter to stop this evaluation in such situationOverall, very good and clever code, I will definitely recommend it to my friends and colleagues!
kzemek
Thanks for the notes, much appreciated!
Right. However, I see tasks holding on to their environments before they finish as a feature, not an edge case. For example, you can start an async task from Elixir and immediately discard your
env:Inside,
some_async_taskcan even send messages back to BEAM processes withsnex.send(), so starting tasks without blocking is a useful pattern for Snex.You’re right, in the current iteration it could be an incrementing integer. At an early point in the design, I had other processes sending commands directly to the port, so ID generation was decentralized. I still want to have that option open for the future.
16 random bytes is around the same entropy as UUIDv4, so I defaulted to that. Now that I think about it, it could also be an
:erlang.unique_integer()instead for 64 bits of space, although shared with other users on the node.Good idea!
Correct. The usual pattern would be exactly to prepare an env and repeatedly reference the object from the env. With various opts of
make_env/3you can also copy Python objects between the envs.I think it makes sense to be able to list running tasks and cancel them from Elixir side. Even a task stuck in an infinite loop with no
awaitsmight be cancellable using OS signals.If you search around for “elixir python” or “erlang python”, there’s a lot of projects that tackled this through C-level integration and are unusable today as they required constant maintenance to stay “fresh”. PyErl was one such project implementing C-node Python.
Making a C-node out of Python is less of a maintenance burden than directly integrating CPython API like pythonx does, but it still requires tracking Erl_Interface which is changing between BEAM versions, sometimes in breaking ways.
An important design goal of Snex is to stay evergreen as much as possible, so the current version is likely to still be usable with Python 3.17/Elixir 1.21/Erlang 30.
Thank you!
kzemek
Happy new year! Snex
0.3is out with some major upgrades. The biggest highlights are the revamped serialization story andawait snex.call(m, f, a)in Python, but there’s been a lot of smaller enhancements all around.Below is the abbreviated changelog. The full one can be found in CHANGELOG.md, and details on current usage patterns can be found in the updated README.md - I find the Encoding/decoding table particularly interesting!
Highlights
Improved serialization
Serialization protocol between Python & Elixir has been changed.
Instead of JSON encoding both ways, Elixir now encodes terms into a restricted Pickle (v5) format, while Python encodes objects into a restricted External Term Format.
The encoded data is then decoded with
pickle.loads()on Python side, and:erlang.binary_to_term/1in Elixir.Writing only encoders in both languages allows the implementation to be small and portable between versions, and reusing native decoding routines makes it highly performant - especially in Python.
Consequently:
:erlang.term_to_binary/1)New ways of calling Elixir code from Python
Code running under Snex can use
snex.call(m, f, a)andsnex.cast(m, f, a)to communicate with the BEAM.snex.callcan be awaited on, and will return the result of the call, whilesnex.castis fire-and-forget (returnsNone).Sigils for code location
import Snex.Sigilsto use~p"code",~P"code"sigils, which create%Snex.Code{}automatically.New
Snex.Interpreter(and custom interpreters) options:label- labels the interpreter process through:proc_lib.set_label/1.:init_script- now also takes{script, args}tuple, to pass variables to the init script and the root environment it prepares.:init_script_timeout- if:init_scriptdoesn’t finish under the timeout, the interpreter process stops with%Snex.Error{code: :init_script_timeout}.:wrap_exec- customizes how the Python process is spawned by wrapping the executable path and arguments.This can be used e.g. to run Python inside a Docker container or set it up with cgroups.
New functions
Snex.destroy_env/1- explicitly cleans up the referenced Python environmentSnex.Env.disable_gc/1- opts out of automatic lifetime management for a%Snex.Env{}Snex.Env.interpreter/1- gets the interpreter process associated with a%Snex.Env{}Snex.Interpreter.os_pid/1- gets the OS PID of the Python interpreter process.Snex.Interpreter.stop/1,2,3- stops the interpreter processMove serde work to Snex callers
Serialization and deserialization on Elixir side is now done outside of
Snex.Interpreterprocess.Move Snex interface code to a public module
snexYou can now import
snexmodule from your external Python code to get awareness of Snex Python-side types and interface.kzemek
Snex 0.4.0 is out after a few weeks of RC production use! This release focuses on performance and devx, and support for using Snex from Python subprocesses.
As before, below is the abbreviated changelog with some of the more interesting changes. The full one can be found in CHANGELOG.md. Changelog entries are written by hand, so it should be very readable.
Highlights
Improved performance
Simple
Snex.pyevalcall latency dropped from 75 µs (v0.3.2) to 38 µs end-to-end on a MacBook M1 - nearly 2× lower. There’s a clear path to 28 μs, slated forv0.4.1.snexPython module documentationsnexPython interface is now documented withExDoc!Breaking changes
Snex.pyeval/4no longer returns bare:okSnex.pyeval/4now always returns either{:ok, value}or{:error, reason}.snex.sendandsnex.castnow requireawaitThe write transport from Python to Elixir is now under standard Python asyncio flow control. As a side effect,
send()&cast()were required to becomeasync.Features
Snex.pyeval/4can execute code with PythonreturnstatementsThe
:returningoption is now deprecated - just writeSnex.pyeval(env, "return 42")Multiprocessing support
snex.serveandsnex.io_loop_for_connectionprovide a clean way to connect Python subprocesses - or any external processes - to the Snex system, enablingsnex.call()&snex.cast()from outside of the main Python process.snex.Elixirproxy objectAdded syntax sugar for
snex.cast()/snex.call()for a natural Elixir-like call syntax in Python:snex.LoggingHandlerSnex Python interface now provides a
snex.LoggingHandlerclass for use with Python’slogging. The handler outputs Python logs using Elixir’sLogger.Snex.pyeval/4can be called with interpreter instead of%Snex.Env{}Simplifies setup and improves latency for one-off commands.
https://github.com/kzemek/snex
kzemek
Snex 0.4.1 is out now!
This release introduces an eager
asyncioloop improvement discussed on the Python forum. This brings the end-to-end latency of a simpleSnex.pyevaldown to 28 μs on CPython, 20 μs on PyPy - closing in on NIF-based integrations. For comparison, an equivalentPythonx.eval+decodeis ~6.5 μs[1].I also revamped the Highlights section of README.md to better showcase what Snex is and can do. I’m putting it down below for a refresher.
Thanks!
Highlights
Robust & Isolated - Run any number of Python interpreters in separate OS processes, preventing GIL issues or blocking computations from affecting your Elixir application. You can call asyncio code, use PyPy instead of CPython, or even run Python in a Docker container!
Declarative Environments - Leverages
uvto manage Python versions and dependencies, embedding them into your application’s release for consistent deployments. Supports custom Python environments and easy integration with Python projects.Bidirectional communication - Powerful and efficient interface with explicit control over data. Python code running under Snex can send messages to BEAM processes and call Erlang/Elixir functions.
High quality, organic code - Every line of Snex is thought out and serves a purpose. Code is optimized to keep performance overhead low.
Forward Compatibility - Built on stable foundations independent of C-level interfaces, so future versions of Python and Elixir will work on day one!
https://github.com/kzemek/snex
in local serialized benchmarks on an M1 Macbook Pro. The latency story is completely different in concurrent usage, where
Pythonxgets blocked on GIL. ↩︎nathanl
“snex” - love the name

kzemek
Snex 0.4.2 is out! This patch release contains a fix that works around a BEAM Port resource leak, which can permanently stall communication through a port.