SnakeBridge lets you call Python from Elixir with type-safe bindings. This guide covers everything you need to start using Python libraries in your Elixir project.
Prerequisites
Before installing SnakeBridge, ensure you have:
- Elixir 1.14+ - Check with
elixir --version - Python 3.8+ - Check with
python3 --version - uv - Fast Python package manager required by Snakepit
Installing uv
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or via Homebrew
brew install uv
Installation
Add SnakeBridge to your mix.exs with the Python libraries you want to use:
defmodule MyApp.MixProject do
use Mix.Project
def project do
[
app: :my_app,
version: "1.0.0",
elixir: "~> 1.14",
deps: deps(),
python_deps: python_deps(),
# Required: Add the snakebridge compiler
compilers: [:snakebridge] ++ Mix.compilers()
]
end
defp deps do
[{:snakebridge, "~> 0.15.0"}]
end
# Python dependencies - just like Elixir deps
defp python_deps do
[
{:numpy, "1.26.0"},
{:pandas, "2.0.0", include: ["DataFrame", "read_csv"]}
]
end
endThe compilers: [:snakebridge] ++ Mix.compilers() line enables automatic Python package installation, type introspection, and wrapper generation at compile time.
Then add runtime configuration in config/runtime.exs:
import Config
SnakeBridge.ConfigHelper.configure_snakepit!()Finally, fetch dependencies and compile:
mix deps.get
mix compile
Quick Start Example
Simple Function Call
Call any Python function with SnakeBridge.call/4:
# Call math.sqrt(16)
{:ok, result} = SnakeBridge.call("math", "sqrt", [16])
# result = 4.0
# With keyword arguments: round(3.14159, ndigits=2)
{:ok, rounded} = SnakeBridge.call("builtins", "round", [3.14159], ndigits: 2)
# rounded = 3.14
# Submodule paths work directly
{:ok, path} = SnakeBridge.call("os.path", "join", ["/home", "user", "file.txt"])Creating and Using Python Objects (Refs)
When you create a Python object, SnakeBridge returns a "ref" - a handle to the object living in Python memory:
# Create a pathlib.Path object
{:ok, path} = SnakeBridge.call("pathlib", "Path", ["/tmp/example.txt"])
# Check if it's a ref
SnakeBridge.ref?(path) # true
# Call methods on the ref
{:ok, exists?} = SnakeBridge.method(path, "exists", [])
# Access attributes
{:ok, name} = SnakeBridge.attr(path, "name") # "example.txt"
{:ok, suffix} = SnakeBridge.attr(path, "suffix") # ".txt"
# Method chaining - parent returns another ref
{:ok, parent} = SnakeBridge.attr(path, "parent")
{:ok, parent_name} = SnakeBridge.attr(parent, "name") # "tmp"Getting Module Constants
Access module-level constants with SnakeBridge.get/3:
{:ok, pi} = SnakeBridge.get("math", "pi") # 3.141592653589793
{:ok, e} = SnakeBridge.get("math", "e") # 2.718281828459045
{:ok, sep} = SnakeBridge.get("os", "sep") # "/" on UnixBang Variants
Use bang variants to raise on errors instead of pattern matching:
result = SnakeBridge.call!("math", "sqrt", [16])
pi = SnakeBridge.get!("math", "pi")
path = SnakeBridge.call!("pathlib", "Path", ["."])
exists? = SnakeBridge.method!(path, "exists", [])
name = SnakeBridge.attr!(path, "name")Two Ways to Call Python
SnakeBridge offers two approaches that can coexist in the same project.
1. Universal FFI (Runtime, Flexible)
The Universal FFI lets you call any Python module dynamically without code generation:
{:ok, result} = SnakeBridge.call("json", "dumps", [%{name: "test"}])
{:ok, hash_obj} = SnakeBridge.call("hashlib", "md5", [SnakeBridge.bytes("abc")])
{:ok, hex} = SnakeBridge.method(hash_obj, "hexdigest", [])Use Universal FFI when:
- Calling libraries not in your
python_deps - Module paths are determined at runtime
- Writing quick scripts or one-off calls
- Accessing stdlib modules (math, json, os, etc.)
2. Generated Wrappers (Compile-time, Typed)
Libraries in python_deps get Elixir wrapper modules with type hints and docs:
# In mix.exs
defp python_deps do
[{:numpy, "1.26.0"}, {:pandas, "2.0.0", include: ["DataFrame"]}]
end
# After compilation, use like native Elixir
{:ok, result} = Numpy.mean([1, 2, 3, 4])
{:ok, result} = Numpy.mean([[1, 2], [3, 4]], axis: 0)
# Classes generate new/N constructors
{:ok, df} = Pandas.DataFrame.new(%{"a" => [1, 2], "b" => [3, 4]})Use Generated Wrappers when:
- You have core libraries you call frequently
- You want compile-time type hints and ExDoc documentation
- You want IDE autocomplete and signature validation
Comparison
| Feature | Universal FFI | Generated Wrappers |
|---|---|---|
| Setup | None | Add to python_deps |
| Type hints / IDE support | No | Yes |
| Compile-time checks | No | Yes |
| Any module | Yes | Only configured |
Both can coexist. A typical project might use generated wrappers for NumPy and Pandas, with Universal FFI for one-off stdlib calls.
Running Python Code
For scripts and Mix tasks, use SnakeBridge.script/1:
SnakeBridge.script do
{:ok, result} = SnakeBridge.call("math", "sqrt", [16])
IO.inspect(result)
endThis ensures proper startup and shutdown of the Python process pool. For custom lifecycle
options, use SnakeBridge.run_as_script/2.
Next Steps
Explore these guides for more advanced usage:
- Universal FFI Guide - Complete reference for runtime Python calls
- Generated Wrappers Guide - Configuring
python_depsand wrapper generation - Refs and Sessions Guide - Managing Python object lifecycles
- Session Affinity Guide - Routing stateful calls to the same worker
- Type System Guide - Data encoding between Elixir and Python
- Error Handling Guide - Structured error translation
- Best Practices Guide - Patterns and recommendations
See Also
- API Documentation
- Examples
- Snakepit - The underlying Python process pool