For Livebook, this looks really cool. Love that it calls CPython directly via C++ NIFS in Elixir and returns Elixir-native data structures. That's a lot cleaner than interacting with Python in Elixir via Ports, which is essentially executing a `python` command under the hood.
For production servers, Pythonx is a bit more risky (and the developers aren't claiming it's the right tool for this use case). Because it's running on the same OS process as your Elixir app, you bypass the failure recovery that makes an Elixir/BEAM application so powerful.
Normally, an Elixir app has a supervision tree that can gracefully handle failures of its own BEAM processes (an internal concurrency unit -- kind like a synthetic OS process) and keep the rest of the app's processes running. That's one of the big selling points of languages like Elixir, Erlang, and Gleam that build upon the BEAM architecture.
Because it uses NIFs (natively-implemented functions), an unhandled exception in Pythonx would take down your whole OS process along with all other BEAM processes, making your supervision tree a bit worthless in that regard.
There are cases when NIFs are super helpful (for instance, Rustler is a popular NIF wrapper for Rust in Elixir), but you have to architect around the fact that it could take down the whole app. Using Ports (Erlang and Elixir's long-standing external execution handler) to run other native code like Python or Rust is less risky in this respect because the non-Elixir code it's still running in a separate OS process.
One possibility for production use (in case there is a big value) is to split the nodes into one "front" node which requires strong uptime, and a "worker" node which is designed to support rare crashes gracefully, in a way that does not impact the front.
> an unhandled exception in Pythonx would take down your whole OS
Is there a class of exceptions that wouldn't be caught by PythonX's wrapper? FTA (with emphasis added):
> Pythonx ties Python and Erlang garbage collection, so that the objects can be safely kept between evaluations. Also, it conveniently handles conversion between Elixir and Python data structures, bubbles Python exceptions and captures standard output.
And...
> Rustler is a popular NIF wrapper for Rust in Elixir
From Rustler's Git README:
> The code you write in a Rust NIF should never be able to crash the BEAM.
I haven't used Rustler, Zigler or PythonX (yet), so I'm genuinely asking if I'm mistaken in my understanding of their safety.
Do any of them communicate with the BEAM? There used to be a Go based implementation of the BEAM that allowed you to drop-in with Go, I have to wonder if this could be done with Python so it doesn't interfere with what the BEAM is good that and lets Python code remain as-is.
> Because it uses NIFs (natively-implemented functions), an unhandled exception in Pythonx would take down your whole OS process along with all other BEAM processes, making your supervision tree a bit worthless in that regard.
What's the Elixir equivalent if "Pythonic"? An architecture that allows a NIF to take down your entire supervision tree is the opposite of that, as it defeats a the stacks' philosophy.
The best practice for integrating Python into Elixir or Erlang would be to have an assigned genserver, or other supervision-tree element - responsible for hosting the Python NIF(s), and the design should allow for each branch or leaf of that tree to be killed/restarted safely, with no loss of state. BEAM message passing is cheap
I love to see "well-known" people in the Elixir community endorsing and actively developing that kind of approach. Our VM and runtime does so much and is so well suited to orchestrating other languages and tech that it sometimes feels there's a standard track and an off-road track.
The difference between an off-road "sounds dangerous" idea and its safe execution is often only the quantity of work but our runtime encourages that. Here, it's a NIF so there's still a bit of risk, but it's always possible to spawn a separate BEAM instance and distribute yourself with it.
Toy example that illustrates it, first crashing with a NIF that is made to segfault :
Great and informative article. Also nice to get an explicit mention that this isn't just a subprocess call, but running in the same process.
The only thing I'd would have like to see in added would be calling a function defined in Python from Elixir, instead of only the `Pythonx.eval` example.
The `%{"binary" => binary}` is very telling, but a couple of more and different examples would have been nice.
Other commenters have already pointed out the safety implications of using NIFs for this. There are, however, other downsides worth considering:
- The Erlang VM scheduler can't preempt a NIF, so a long-running Python call risks hanging the VM. This is a non-issue for ports, since Python's running in a separate OS process. A NIF can mitigate this by spawning an OS thread and yielding until it finishes; ain't clear if that's what this library is doing.
- The article already mentions that the GIL prevents concurrent Python execution, but this would also be a non-issue for ports, since the Erlang caller would just spin up multiple Python interpreters. Does Python allow multiple interpreters per (OS) process, like e.g. Tcl does? If so, then that'd be a possible avenue for mitigating this issue while sticking with NIFs.
I would have guess the builders of this would have mitigated this problem by running the python in a thread? That won't hang the VM (or cause a segfault at the 1ms boundary). It might cause OS starvation in extreme cases, but you'd have to be really extreme.
Forgive some ignorance on this; why is Elixir a better fit for AI than Python or JavaScript? I'm not disagreeing, I've just never heard that, I didn't think that Elixir had good linear algebra libraries like NumPy.
I love the initial decision to grow Elixir's ML foundations from scratch, but I also love that we now have a really ergonomic way to farm out to the fast-moving python libraries
> Also, it conveniently handles conversion between Elixir and Python data structures, bubbles Python exceptions and captures standard output
At first read this seems really promising. Getting into Elixir/Erlang ecosystem from Python has seemed too hard to take the time. And when there I wouldn't be able to leverage all the Python stuff I've learned. With Pythonx gradual learning seems now much more achievable.
It wasn't mentioned in the article, but there's older blog post on fly.io [1] about live book, GPUs, and their FLAME serverless pattern [2]. Since there seems to be some common ground between these companies I'm now hoping Pythonx support is coming to FLAME enabled Erlang VM. I'm just going off from the blog posts, and am probably using wrong terminology here.
For Python's GIL problem mentioned in the article I wonder if they have experimented with free threading [3].
FLAME runs the same code base on another machine. FLAME with Pythonx should just work. FLAME is a set of nice abstractions on top of a completely regular Erlang VM.
Chris Grainger who pushed for the value of Python in Livebook has given at least two talks about the power and value of FLAME.
And of course Chris McCord (creator of Phoenix and FLAME) works at Fly and collaborates closely with Dashbit who do Livebook and all that.
These are some of the benefits of a cohesive ecosystem. Something I enjoy a lot in Elixir. All these efforts are aligned. There is nothing weird going on, no special work you need to do.
I'll add: FLAME is probably a great addition to pythonx. While a NIF can crash the node it is executed on, FLAME calls are executed on other nodes by default. So a crash here would only hard-crash processes on the same node (FLAME lets you group calls so that a flame node can have many being executed on it at any time).
Errors bubble back up to the calling process (and crash it by default but can be handled explicitly), so managing and retrying failed calls is easy.
- everything (or most things) is a macro, even def, etc.
- pipes |>, and no, I don't want to write a "pipe" class in Python to use it like pipe(foo, bar, ...). 90% of the |> power comes from its 'flow' programming style.
- true immutability
- true parallelism and concurrency thanks to the supervision trees
- hot code reloading (you recompile the app WHILE it's running)
- fault tolerance (again, thanks for supervision trees)
Coming from Erlang, I think macros are one of the things I'm ambivalent about in Elixir. There are a bunch of actual improvements besides just the syntax itself in Elixir, like string handling, but things like macros in Ecto ... not yet a fan of that.
You can abuse the '>>' notation in python for pipes (or you could use |, I suppose), but you'll have to deal with whitespace shenanigans. I'm also not entirely sure about the order of evaluation. And you'll need to do partial function application by hand if you want that (though it is possible to write a meta function for that).
The part of Elixir pipes that people overlook is the fact that, as a native, heavily used part of the language, the entirety of the standard library and most third party libraries are written with their behavior in mind. The first argument (the pipe target) will be the argument you want piped in faaaar more often than in languages where pipes are tacked on later.
As someone very involved in Elixir and who used to do a lot of Python this seems very practical for me. I'm actually even more interested in that Fine library for making C++ NIFs easy. That seems ridiculously valuable for removing hurdles to building library bindings.
Elixir is just Lisp with a facelift[1], and lisps can be built on Python[2]. It stands to reason that an elixir-like can be built on Python too, so you could embed the Python runtime in Elixir but Elixir-likes are used to code for both.
[+] [-] bicx|1 year ago|reply
For production servers, Pythonx is a bit more risky (and the developers aren't claiming it's the right tool for this use case). Because it's running on the same OS process as your Elixir app, you bypass the failure recovery that makes an Elixir/BEAM application so powerful.
Normally, an Elixir app has a supervision tree that can gracefully handle failures of its own BEAM processes (an internal concurrency unit -- kind like a synthetic OS process) and keep the rest of the app's processes running. That's one of the big selling points of languages like Elixir, Erlang, and Gleam that build upon the BEAM architecture.
Because it uses NIFs (natively-implemented functions), an unhandled exception in Pythonx would take down your whole OS process along with all other BEAM processes, making your supervision tree a bit worthless in that regard.
There are cases when NIFs are super helpful (for instance, Rustler is a popular NIF wrapper for Rust in Elixir), but you have to architect around the fact that it could take down the whole app. Using Ports (Erlang and Elixir's long-standing external execution handler) to run other native code like Python or Rust is less risky in this respect because the non-Elixir code it's still running in a separate OS process.
[+] [-] thibaut_barrere|1 year ago|reply
This is what we use at https://transport.data.gouv.fr/ (the French National Access Point for transportation data - more background at https://elixir-lang.org/blog/2021/11/10/embracing-open-data-...).
Note that we're not using Pythonx, but running some memory hungry processes which can sometime take the worker node down.
[+] [-] h0l0cube|1 year ago|reply
Is there a class of exceptions that wouldn't be caught by PythonX's wrapper? FTA (with emphasis added):
> Pythonx ties Python and Erlang garbage collection, so that the objects can be safely kept between evaluations. Also, it conveniently handles conversion between Elixir and Python data structures, bubbles Python exceptions and captures standard output.
And...
> Rustler is a popular NIF wrapper for Rust in Elixir
From Rustler's Git README:
> The code you write in a Rust NIF should never be able to crash the BEAM.
I haven't used Rustler, Zigler or PythonX (yet), so I'm genuinely asking if I'm mistaken in my understanding of their safety.
[+] [-] chefandy|1 year ago|reply
[+] [-] giancarlostoro|1 year ago|reply
[+] [-] alienthrowaway|1 year ago|reply
What's the Elixir equivalent if "Pythonic"? An architecture that allows a NIF to take down your entire supervision tree is the opposite of that, as it defeats a the stacks' philosophy.
The best practice for integrating Python into Elixir or Erlang would be to have an assigned genserver, or other supervision-tree element - responsible for hosting the Python NIF(s), and the design should allow for each branch or leaf of that tree to be killed/restarted safely, with no loss of state. BEAM message passing is cheap
[+] [-] chantepierre|1 year ago|reply
The difference between an off-road "sounds dangerous" idea and its safe execution is often only the quantity of work but our runtime encourages that. Here, it's a NIF so there's still a bit of risk, but it's always possible to spawn a separate BEAM instance and distribute yourself with it.
Toy example that illustrates it, first crashing with a NIF that is made to segfault :
In the second example, we have a "SafeNif" module that spawns another elixir node, connects to it, and runs the unsafe operation on it. Thankfully Python, Zig and Rust should be good to go without that kind of dance :) .[+] [-] tommica|1 year ago|reply
[+] [-] qwertox|1 year ago|reply
The only thing I'd would have like to see in added would be calling a function defined in Python from Elixir, instead of only the `Pythonx.eval` example.
The `%{"binary" => binary}` is very telling, but a couple of more and different examples would have been nice.
[+] [-] yellowapple|1 year ago|reply
- The Erlang VM scheduler can't preempt a NIF, so a long-running Python call risks hanging the VM. This is a non-issue for ports, since Python's running in a separate OS process. A NIF can mitigate this by spawning an OS thread and yielding until it finishes; ain't clear if that's what this library is doing.
- The article already mentions that the GIL prevents concurrent Python execution, but this would also be a non-issue for ports, since the Erlang caller would just spin up multiple Python interpreters. Does Python allow multiple interpreters per (OS) process, like e.g. Tcl does? If so, then that'd be a possible avenue for mitigating this issue while sticking with NIFs.
[+] [-] throwawaymaths|1 year ago|reply
[+] [-] abrookewood|1 year ago|reply
[+] [-] cpursley|1 year ago|reply
[+] [-] tombert|1 year ago|reply
[+] [-] jwbaldwin|1 year ago|reply
> Also, it conveniently handles conversion between Elixir and Python data structures, bubbles Python exceptions and captures standard output
Sooo nice
[+] [-] jarpineh|1 year ago|reply
It wasn't mentioned in the article, but there's older blog post on fly.io [1] about live book, GPUs, and their FLAME serverless pattern [2]. Since there seems to be some common ground between these companies I'm now hoping Pythonx support is coming to FLAME enabled Erlang VM. I'm just going off from the blog posts, and am probably using wrong terminology here.
For Python's GIL problem mentioned in the article I wonder if they have experimented with free threading [3].
[1] https://fly.io/blog/ai-gpu-clusters-from-your-laptop-liveboo...
[2] https://fly.io/blog/rethinking-serverless-with-flame/
[3] https://docs.python.org/3/howto/free-threading-python.html
[+] [-] lawik|1 year ago|reply
Chris Grainger who pushed for the value of Python in Livebook has given at least two talks about the power and value of FLAME.
And of course Chris McCord (creator of Phoenix and FLAME) works at Fly and collaborates closely with Dashbit who do Livebook and all that.
These are some of the benefits of a cohesive ecosystem. Something I enjoy a lot in Elixir. All these efforts are aligned. There is nothing weird going on, no special work you need to do.
[+] [-] solid_fuel|1 year ago|reply
I'll add: FLAME is probably a great addition to pythonx. While a NIF can crash the node it is executed on, FLAME calls are executed on other nodes by default. So a crash here would only hard-crash processes on the same node (FLAME lets you group calls so that a flame node can have many being executed on it at any time).
Errors bubble back up to the calling process (and crash it by default but can be handled explicitly), so managing and retrying failed calls is easy.
[+] [-] behnamoh|1 year ago|reply
- atoms
- everything (or most things) is a macro, even def, etc.
- pipes |>, and no, I don't want to write a "pipe" class in Python to use it like pipe(foo, bar, ...). 90% of the |> power comes from its 'flow' programming style.
- true immutability
- true parallelism and concurrency thanks to the supervision trees
- hot code reloading (you recompile the app WHILE it's running)
- fault tolerance (again, thanks for supervision trees)
[+] [-] ch4s3|1 year ago|reply
[+] [-] davidw|1 year ago|reply
[+] [-] shiandow|1 year ago|reply
So one could write
but whether that is a good idea is a whole different matter.[+] [-] OkayPhysicist|1 year ago|reply
[+] [-] lawik|1 year ago|reply
[+] [-] crenwick|1 year ago|reply
[+] [-] ejs|1 year ago|reply
[+] [-] pmarreck|1 year ago|reply
[+] [-] unknown|1 year ago|reply
[deleted]
[+] [-] incontrol|1 year ago|reply
"...if you are using this library to integrate with Python, make sure it happens in a single Elixir process..."
[+] [-] nesarkvechnep|1 year ago|reply
[+] [-] ddanieltan|1 year ago|reply
just kidding, this is pretty cool.
[+] [-] 4b11b4|1 year ago|reply
[+] [-] djha-skin|1 year ago|reply
1: https://wiki.alopex.li/ElixirForCynicalCurmudgeons
2: https://hylang.org/
[+] [-] ch4s3|1 year ago|reply
[+] [-] pjmlp|1 year ago|reply
Could be better, but that is what mainstream gets.