kzemek

kzemek

Snex - Easy and efficient Python interop for Elixir

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 uv to 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, asyncio code, 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: :snake: Easy and efficient Python interop for Elixir
Hex: snex | Hex
HexDocs: snex v0.2.0 — Documentation

https://github.com/kzemek/snex

Most Liked Responses

kzemek

kzemek

Snex 0.4.1 is out now!

This release introduces an eager asyncio loop improvement discussed on the Python forum. This brings the end-to-end latency of a simple Snex.pyeval down to 28 μs on CPython, 20 μs on PyPy - closing in on NIF-based integrations. For comparison, an equivalent Pythonx.eval+decode is ~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 uv to 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


  1. in local serialized benchmarks on an M1 Macbook Pro. The latency story is completely different in concurrent usage, where Pythonx gets blocked on GIL. ↩︎

kzemek

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:

Snex.pyeval(env, """
  import asyncio
  loop = asyncio.get_event_loop()
  loop.call_soon(some_async_task)
  """)

Inside, some_async_task can even send messages back to BEAM processes with snex.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/3 you 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 awaits might 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

kzemek

Happy new year! Snex 0.3 is out with some major upgrades. The biggest highlights are the revamped serialization story and await 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/1 in 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:

    • tuples are now encoded as tuples instead of lists
    • all Elixir terms can be encoded and round-tripped (fallback to :erlang.term_to_binary/1)
    • all Python basic objects can be encoded
    • floats preserve representation between languages
    • encoding customization is available on both sides
  • New ways of calling Elixir code from Python

    Code running under Snex can use snex.call(m, f, a) and snex.cast(m, f, a) to communicate with the BEAM.
    snex.call can be awaited on, and will return the result of the call, while snex.cast is fire-and-forget (returns None).

  • Sigils for code location

    import Snex.Sigils to use ~p"code", ~P"code" sigils, which create %Snex.Code{} automatically.

    iex> Snex.pyeval(env, ~p"raise RuntimeError('test')")
    
    {:error,
      %Snex.Error{
      traceback: [...,
        "  File \"/Users/me/snex/CHANGELOG.md\", line 41, in <module>\n    raise RuntimeError(\"test\")\n",
        "RuntimeError: test\n"]
      }}
    
  • 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_script doesn’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 environment
    • Snex.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 process
  • Move serde work to Snex callers

    Serialization and deserialization on Elixir side is now done outside of Snex.Interpreter process.

  • Move Snex interface code to a public module snex

    You can now import snex module from your external Python code to get awareness of Snex Python-side types and interface.

Where Next?

Popular in Announcing Top

mspanc
I am pleased to announce an initial release of the Membrane Framework - an Elixir-based framework with special focus on processing multim...
New
Crowdhailer
I have been updating a library that allows you to pipe between functions that use the erlang result tuple convention. Assuming you have ...
New
mbuhot
Leverage Open Api 3.0 (Swagger) to document, test, validate and explore your Plug and Phoenix APIs. Generate and serve a JSON Open API ...
New
sbs
Only 650 LOC, wrote for fun :slight_smile: https://github.com/sunboshan/qrcode
New
Azolo
Hey everyone, I just released WebSockex which is a Elixir WebSocket client. WebSockex strives to work as a OTP special process, be RFC6...
New
hpopp
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New
benlime
LiveMotion enables high performance animations declared on the server and run on the client. As a follow up to my previous thread A libr...
New
scohen
Lexical Lexical is a next-generation language server for the Elixir programming language. Features Context aware code completion As-you...
New
wfgilman
I’ve cleaned up and open sourced three financial libraries I was using for my company. They are bindings for the APIs of these three comp...
New
trisolaran
Hi! :waving_hand: I would like to present LiveSelect, a little library that I wrote to easily add a dynamic selection input to your LV f...
198 10858 107
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

We're in Beta

About us Mission Statement