Getting Started with SnakeBridge

Copy Markdown View Source

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:

  1. Elixir 1.14+ - Check with elixir --version
  2. Python 3.8+ - Check with python3 --version
  3. 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
end

The 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 Unix

Bang 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

FeatureUniversal FFIGenerated Wrappers
SetupNoneAdd to python_deps
Type hints / IDE supportNoYes
Compile-time checksNoYes
Any moduleYesOnly 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)
end

This 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:

See Also