Agent Skills: Convert Python to Elixir

Convert Python code to Elixir. Use when migrating Python projects to Elixir, translating Python patterns to idiomatic Elixir, refactoring Python codebases into BEAM/OTP, or building Elixir services from Python prototypes. Extends meta-convert-dev with Python-to-Elixir specific patterns covering all 10 pillars including FFI and Dev Workflow.

UncategorizedID: arustydev/ai/convert-python-elixir

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/skills/convert-python-elixir

Skill Files

Browse the full folder contents for convert-python-elixir.

Download Skill

Loading file tree…

content/skills/convert-python-elixir/SKILL.md

Skill Metadata

Name
convert-python-elixir
Description
Convert Python code to Elixir. Use when migrating Python projects to Elixir, translating Python patterns to idiomatic Elixir, refactoring Python codebases into BEAM/OTP, or building Elixir services from Python prototypes. Extends meta-convert-dev with Python-to-Elixir specific patterns covering all 10 pillars including FFI and Dev Workflow.

Convert Python to Elixir

Convert Python code to idiomatic Elixir. This skill extends meta-convert-dev with Python-to-Elixir specific type mappings, idiom translations, OTP/BEAM concurrency patterns, and comprehensive coverage of all 10 conversion pillars.

This Skill Extends

  • meta-convert-dev - Foundational conversion patterns (APTV workflow, testing strategies)

This Skill Adds

  • Type mappings: Python types → Elixir types (dynamic → dynamic with pattern matching)
  • Idiom translations: Python patterns → idiomatic Elixir functional patterns
  • Error handling: try/except → with/case, {:ok, _}/{:error, _} tuples
  • Concurrency: threading/asyncio → GenServer, Task, Agent, OTP supervision
  • Module system: packages/modules → nested Elixir modules
  • Metaprogramming: decorators → macros, behaviours
  • Serialization: Pydantic/dataclasses → Ecto schemas, Jason
  • Build/Deps: pip/poetry → mix, hex
  • Dev Workflow: Python REPL → IEx, Mix tasks, hot code reloading
  • FFI/Interop: C extensions/ctypes → NIFs, Ports, Rustler

When to Use This Conversion

  • Migrating to BEAM for fault tolerance and soft real-time capabilities
  • Scaling concurrent systems - Python GIL limitations → true parallelism
  • Building distributed systems - Leveraging Erlang/OTP battle-tested patterns
  • Hot code reloading - Production systems requiring zero-downtime updates
  • Functional refactoring - Moving from OOP/imperative to functional paradigm
  • Phoenix web apps - Python web services → Phoenix framework
  • Low-latency systems - Predictable performance without GC pauses

When NOT to Use This Conversion

  • Data science pipelines - Python ecosystem (NumPy, Pandas, scikit-learn) is unmatched
  • Machine learning - Keep Python for TensorFlow, PyTorch, etc.
  • Quick prototypes - Python is faster for throwaway scripts
  • Heavy C integration - Python's C API is more mature than NIFs
  • Team expertise - Team unfamiliar with functional programming or Erlang VM

Quick Reference

| Python | Elixir | Notes | |--------|--------|-------| | None | nil | Both falsy, but Elixir has explicit pattern matching | | True/False | true/false | Atoms in Elixir | | dict | %{} (map) | Immutable in Elixir | | list | [] (list) | Immutable, linked list in Elixir | | tuple | {} (tuple) | Immutable in both | | set | MapSet | Module-based in Elixir | | def function(): | def function do ... end | Elixir uses do/end blocks | | class MyClass: | defmodule MyModule do | Modules instead of classes | | try/except | try/rescue or with/case | Prefer pattern matching | | @decorator | @behaviour or macros | Different metaprogramming model | | threading.Thread | Task or spawn | Lightweight BEAM processes | | asyncio | Task.async or GenServer | Actor model, not event loop | | if __name__ == "__main__": | Mix tasks | Build tool integration |


APTV Workflow for Python → Elixir

Every Python-to-Elixir conversion follows: Analyze → Plan → Transform → Validate

1. ANALYZE Phase

Understand the Python codebase:

# Analyze Python structure
project/
├── src/
│   ├── __init__.py
│   ├── models.py       # Dataclasses, Pydantic models
│   ├── services.py     # Business logic
│   ├── api.py          # Flask/FastAPI endpoints
│   └── utils.py        # Helper functions
├── tests/
│   └── test_services.py
├── requirements.txt
└── pyproject.toml

Key analysis points:

  1. Identify concurrency patterns - threading, asyncio, multiprocessing
  2. Map class hierarchies - OOP → functional decomposition
  3. Note mutable state - Global variables, class attributes
  4. Find decorators - @property, @staticmethod, custom decorators
  5. Catalog dependencies - requests, pydantic, sqlalchemy, etc.
  6. Error handling style - Exception hierarchy, custom errors
  7. Entry points - CLI scripts, web servers, workers

2. PLAN Phase

Design Elixir architecture:

# Planned Elixir structure
my_app/
├── lib/
│   ├── my_app/
│   │   ├── models/       # Ecto schemas
│   │   ├── services/     # Business logic modules
│   │   ├── api/          # Phoenix controllers
│   │   └── utils.ex      # Helper modules
│   ├── my_app.ex         # Application entry point
│   └── my_app_web/       # Phoenix web layer
├── test/
│   └── my_app/
│       └── services_test.exs
├── mix.exs
└── config/

Create mapping tables:

| Python Component | Elixir Component | Strategy | |------------------|------------------|----------| | Pydantic models | Ecto schemas | Validation → changeset | | Flask routes | Phoenix routes | Controllers + views | | threading.Thread | Task.async | Supervised tasks | | SQLAlchemy | Ecto | Query DSL, schemas | | pytest fixtures | ExUnit setup callbacks | Test context | | @decorator | Macro or behaviour | Context-dependent | | Global state | GenServer or Agent | Process-based state |

3. TRANSFORM Phase

Convert systematically:

  1. Module structure first - Create Elixir module hierarchy
  2. Data types - Pydantic/dataclasses → Ecto schemas or structs
  3. Pure functions - Easiest conversions, no side effects
  4. Stateful components - Classes → GenServers or Agents
  5. Concurrency - async/threading → Task/GenServer/Supervisor
  6. Error handling - Exceptions → tagged tuples and pattern matching
  7. Tests - pytest → ExUnit with doctests

Golden Rule: Write idiomatic Elixir, not "Python in Elixir syntax"

4. VALIDATE Phase

  1. Functional equivalence - Same inputs → same outputs
  2. Property-based tests - Use StreamData for edge cases
  3. Concurrent behavior - Test supervision trees, process isolation
  4. Performance - Benchmark against Python (expect gains in concurrency)
  5. Integration tests - External dependencies (databases, APIs)

10 Pillars of Python → Elixir Conversion

Pillar 1: Module System

Python Package Structure

# my_package/__init__.py
from .models import User, Order
from .services import UserService

__all__ = ['User', 'Order', 'UserService']

# my_package/models.py
class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

# my_package/services.py
from .models import User

class UserService:
    def create_user(self, name: str, email: str) -> User:
        return User(name, email)

Elixir Module Structure

# lib/my_app/models/user.ex
defmodule MyApp.Models.User do
  defstruct [:name, :email]

  @type t :: %__MODULE__{
    name: String.t(),
    email: String.t()
  }
end

# lib/my_app/models/order.ex
defmodule MyApp.Models.Order do
  defstruct [:id, :user_id, :total]
end

# lib/my_app/services/user_service.ex
defmodule MyApp.Services.UserService do
  alias MyApp.Models.User

  @spec create_user(String.t(), String.t()) :: User.t()
  def create_user(name, email) do
    %User{name: name, email: email}
  end
end

# lib/my_app.ex (Application entry point)
defmodule MyApp do
  @moduledoc """
  MyApp application entry point.
  """

  alias MyApp.Models.{User, Order}
  alias MyApp.Services.UserService

  # Re-export commonly used modules
  defdelegate create_user(name, email), to: UserService
end

Key Differences:

| Python | Elixir | Notes | |--------|--------|-------| | __init__.py re-exports | Main module with alias and defdelegate | Explicit module structure | | import statement | alias, import, require | Elixir distinguishes module vs macro import | | Relative imports | Fully qualified names | MyApp.Models.User is explicit | | __all__ | Module docs + public functions | Only def is public, defp is private |

Import Patterns

# Python imports
from my_package.models import User
from my_package.services import UserService
import my_package.utils as utils
# Elixir aliases
alias MyApp.Models.User
alias MyApp.Services.UserService
alias MyApp.Utils, as: Utils

# Or group aliases
alias MyApp.{Models.User, Services.UserService}

Pillar 2: Error Handling

Python Exception Model

# Python: Exception-based
class UserNotFoundError(Exception):
    def __init__(self, user_id: int):
        self.user_id = user_id
        super().__init__(f"User {user_id} not found")

def get_user(user_id: int) -> dict:
    try:
        user = database.find_user(user_id)
        if user is None:
            raise UserNotFoundError(user_id)
        return user
    except DatabaseError as e:
        logger.error(f"Database error: {e}")
        raise
    finally:
        database.close()

Elixir Tagged Tuple Model

# Elixir: Tagged tuples with pattern matching
defmodule MyApp.Users do
  @type user :: %{id: integer(), name: String.t()}
  @type error_reason :: :not_found | :database_error | :invalid_id

  @spec get_user(integer()) :: {:ok, user()} | {:error, error_reason()}
  def get_user(user_id) when is_integer(user_id) and user_id > 0 do
    case Database.find_user(user_id) do
      {:ok, nil} ->
        {:error, :not_found}

      {:ok, user} ->
        {:ok, user}

      {:error, reason} ->
        Logger.error("Database error: #{inspect(reason)}")
        {:error, :database_error}
    end
  end

  def get_user(_invalid_id), do: {:error, :invalid_id}
end

Using with for Error Chains

# Python: nested try/except
def process_order(order_id: int) -> dict:
    try:
        order = get_order(order_id)
        user = get_user(order['user_id'])
        payment = process_payment(order['total'])
        return {'order': order, 'user': user, 'payment': payment}
    except UserNotFoundError:
        raise OrderProcessingError("User not found")
    except PaymentError:
        raise OrderProcessingError("Payment failed")
# Elixir: with construct for happy path
defmodule MyApp.Orders do
  def process_order(order_id) do
    with {:ok, order} <- get_order(order_id),
         {:ok, user} <- get_user(order.user_id),
         {:ok, payment} <- process_payment(order.total) do
      {:ok, %{order: order, user: user, payment: payment}}
    else
      {:error, :not_found} ->
        {:error, :user_not_found}

      {:error, :payment_failed} = error ->
        Logger.error("Payment failed for order #{order_id}")
        error

      error ->
        {:error, {:processing_failed, error}}
    end
  end
end

Error Translation Table

| Python Pattern | Elixir Pattern | Use Case | |----------------|----------------|----------| | raise ValueError | {:error, :invalid_value} | Input validation | | raise CustomError | {:error, :custom_reason} | Domain errors | | try/except/finally | try/rescue/after (rare) | Only for exceptional cases | | assert condition | unless condition, do: {:error, :assertion_failed} | Guards or pattern matching | | if error: return None | {:error, reason} tuple | Explicit error return | | Exception hierarchy | Atom taxonomy | :db_error, :db_connection_error |

When to Use Exceptions in Elixir

# Rare: Use raise for programmer errors (bugs)
def divide(_a, 0), do: raise ArgumentError, "cannot divide by zero"
def divide(a, b), do: a / b

# Preferred: Use tagged tuples for expected errors
def safe_divide(_a, 0), do: {:error, :division_by_zero}
def safe_divide(a, b), do: {:ok, a / b}

# Pattern matching at call site
case safe_divide(10, 2) do
  {:ok, result} -> IO.puts("Result: #{result}")
  {:error, :division_by_zero} -> IO.puts("Cannot divide by zero")
end

Pillar 3: Concurrency Patterns

Python Threading/Asyncio

# Python: Threading
import threading
from queue import Queue

class Worker:
    def __init__(self):
        self.queue = Queue()
        self.thread = threading.Thread(target=self._run)
        self.running = True
        self.thread.start()

    def _run(self):
        while self.running:
            item = self.queue.get()
            self.process(item)
            self.queue.task_done()

    def process(self, item):
        print(f"Processing {item}")

    def submit(self, item):
        self.queue.put(item)

    def stop(self):
        self.running = False
        self.thread.join()

# Python: Asyncio
import asyncio

async def fetch_data(url: str) -> dict:
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()

async def main():
    results = await asyncio.gather(
        fetch_data("http://api1.com"),
        fetch_data("http://api2.com"),
        fetch_data("http://api3.com")
    )
    return results

# Run
asyncio.run(main())

Elixir GenServer + Task

# Elixir: GenServer for stateful worker
defmodule MyApp.Worker do
  use GenServer

  # Client API
  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  def submit(pid, item) do
    GenServer.cast(pid, {:process, item})
  end

  def stop(pid) do
    GenServer.stop(pid)
  end

  # Server callbacks
  @impl true
  def init(:ok) do
    {:ok, %{processed: 0}}
  end

  @impl true
  def handle_cast({:process, item}, state) do
    IO.puts("Processing #{inspect(item)}")
    {:noreply, %{state | processed: state.processed + 1}}
  end
end

# Elixir: Task for async operations
defmodule MyApp.Fetcher do
  def fetch_data(url) do
    # HTTPoison or Req for HTTP
    case HTTPoison.get(url) do
      {:ok, %{body: body}} -> Jason.decode(body)
      error -> error
    end
  end

  def fetch_all(urls) do
    urls
    |> Enum.map(&Task.async(fn -> fetch_data(&1) end))
    |> Enum.map(&Task.await/1)
  end
end

# Usage
urls = ["http://api1.com", "http://api2.com", "http://api3.com"]
results = MyApp.Fetcher.fetch_all(urls)

Supervision Trees

# Python: Manual process management
import signal
import sys

workers = []

def shutdown_handler(signum, frame):
    for worker in workers:
        worker.stop()
    sys.exit(0)

signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)

# Start workers
for i in range(5):
    worker = Worker()
    workers.append(worker)
# Elixir: Supervisor with automatic restart
defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Workers are supervised - they restart on crash
      {MyApp.Worker, name: Worker1},
      {MyApp.Worker, name: Worker2},
      {MyApp.Worker, name: Worker3},

      # Dynamic supervisor for on-demand workers
      {DynamicSupervisor, name: MyApp.DynamicWorkers, strategy: :one_for_one}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

# Dynamically start workers
{:ok, pid} = DynamicSupervisor.start_child(
  MyApp.DynamicWorkers,
  {MyApp.Worker, []}
)

Concurrency Comparison

| Python Pattern | Elixir Pattern | Notes | |----------------|----------------|-------| | threading.Thread(target=fn) | spawn(fn) or Task.start | Elixir processes are lightweight | | threading.Lock | GenServer state | Message passing eliminates locks | | queue.Queue | GenServer or Agent | Built-in message queues per process | | asyncio.gather() | Task.async_stream | Parallel execution with backpressure | | concurrent.futures.ThreadPoolExecutor | Task.Supervisor | Supervised task pools | | Global threading.local() | Process dictionary | Process.put/get (use sparingly) | | multiprocessing.Process | Node-level distribution | BEAM distribution across nodes |


Pillar 4: Metaprogramming

Python Decorators

# Python: Decorator pattern
from functools import wraps
import time

def timeit(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        duration = time.time() - start
        print(f"{func.__name__} took {duration:.2f}s")
        return result
    return wrapper

@timeit
def expensive_operation(n: int) -> int:
    time.sleep(n)
    return n * 2

# Python: Class decorators
def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    def __init__(self):
        self.connection = "connected"

Elixir Macros

# Elixir: Macro for compile-time code generation
defmodule MyApp.Macros do
  defmacro timeit(do: block) do
    quote do
      start = System.monotonic_time(:millisecond)
      result = unquote(block)
      duration = System.monotonic_time(:millisecond) - start
      IO.puts("Operation took #{duration}ms")
      result
    end
  end
end

# Usage
defmodule MyApp.Example do
  require MyApp.Macros
  import MyApp.Macros

  def expensive_operation(n) do
    timeit do
      Process.sleep(n * 1000)
      n * 2
    end
  end
end

Behaviours (Interface Pattern)

# Python: Abstract base class
from abc import ABC, abstractmethod

class Storage(ABC):
    @abstractmethod
    def save(self, key: str, value: any) -> None:
        pass

    @abstractmethod
    def load(self, key: str) -> any:
        pass

class FileStorage(Storage):
    def save(self, key: str, value: any) -> None:
        with open(f"{key}.txt", "w") as f:
            f.write(str(value))

    def load(self, key: str) -> any:
        with open(f"{key}.txt", "r") as f:
            return f.read()
# Elixir: Behaviour (compile-time contract)
defmodule MyApp.Storage do
  @callback save(key :: String.t(), value :: term()) :: :ok | {:error, term()}
  @callback load(key :: String.t()) :: {:ok, term()} | {:error, term()}
end

defmodule MyApp.FileStorage do
  @behaviour MyApp.Storage

  @impl true
  def save(key, value) do
    case File.write("#{key}.txt", inspect(value)) do
      :ok -> :ok
      error -> error
    end
  end

  @impl true
  def load(key) do
    case File.read("#{key}.txt") do
      {:ok, content} -> {:ok, content}
      error -> error
    end
  end
end

# Compile-time check: warns if callbacks not implemented

Metaprogramming Comparison

| Python Pattern | Elixir Pattern | Use Case | |----------------|----------------|----------| | @decorator | Macro with do block | Code transformation | | @property | defstruct with defaults | Data access | | @staticmethod | Module function | Stateless operations | | @classmethod | Module function with pattern matching | Factory patterns | | Abstract base class | @behaviour | Interface contracts | | Metaclass | Macro generating modules | DSL creation | | __getattr__ | Access behaviour | Dynamic attribute access |


Pillar 5: Zero Values and Defaults

Python None and Mutable Defaults

# Python: None as sentinel
def find_user(user_id: int) -> dict | None:
    user = db.query(user_id)
    return user if user else None

# Python: Mutable default arguments (anti-pattern!)
def add_item(item: str, items: list = []):  # WRONG!
    items.append(item)
    return items

# Correct version
def add_item(item: str, items: list | None = None) -> list:
    if items is None:
        items = []
    items.append(item)
    return items

# Python: Dataclass defaults
from dataclasses import dataclass, field

@dataclass
class User:
    name: str
    email: str
    active: bool = True
    tags: list[str] = field(default_factory=list)  # Avoid mutable default

Elixir nil and Immutable Defaults

# Elixir: nil in pattern matching
defmodule MyApp.Users do
  def find_user(user_id) do
    case Repo.get(User, user_id) do
      nil -> {:error, :not_found}
      user -> {:ok, user}
    end
  end

  # Alternative: use Option-like pattern
  def find_user_option(user_id) do
    user = Repo.get(User, user_id)
    if user, do: {:ok, user}, else: {:error, :not_found}
  end
end

# Elixir: Struct defaults (immutable)
defmodule MyApp.User do
  defstruct name: nil,
            email: nil,
            active: true,
            tags: []  # Safe: lists are immutable

  @type t :: %__MODULE__{
    name: String.t() | nil,
    email: String.t() | nil,
    active: boolean(),
    tags: [String.t()]
  }
end

# Usage
user = %MyApp.User{name: "Alice", email: "alice@example.com"}
# user.tags is [] by default, new instance each time

# Function defaults with keyword lists
defmodule MyApp.Query do
  def search(term, opts \\ []) do
    limit = Keyword.get(opts, :limit, 10)
    offset = Keyword.get(opts, :offset, 0)
    # ...
  end
end

# Usage
MyApp.Query.search("elixir")
MyApp.Query.search("elixir", limit: 20)
MyApp.Query.search("elixir", limit: 20, offset: 10)

Default Value Patterns

| Python Pattern | Elixir Pattern | Notes | |----------------|----------------|-------| | value = None | value = nil | Both represent absence | | if value is None: | if is_nil(value) or pattern match | Explicit nil check | | value or default | value \|\| default | Falsy semantics differ | | value if value else default | value \|\| default | Same as above | | dict.get(key, default) | Map.get(map, key, default) | Dict/Map retrieval | | list.append(x) mutates | [x \| list] or list ++ [x] | Immutable prepend/append | | Mutable default args | Keyword list defaults | No mutable defaults issue |


Pillar 6: Serialization and Validation

Python Pydantic

# Python: Pydantic models with validation
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional
from datetime import datetime

class Address(BaseModel):
    street: str
    city: str
    zip_code: str = Field(pattern=r'^\d{5}$')

class User(BaseModel):
    id: int
    name: str = Field(min_length=1, max_length=100)
    email: EmailStr
    age: int = Field(ge=0, le=150)
    address: Optional[Address] = None
    created_at: datetime = Field(default_factory=datetime.now)

    @field_validator('age')
    @classmethod
    def check_adult(cls, v: int) -> int:
        if v < 18:
            raise ValueError('Must be 18 or older')
        return v

    class Config:
        json_encoders = {
            datetime: lambda v: v.isoformat()
        }

# Usage
user_data = {
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
    "age": 25,
    "address": {
        "street": "123 Main St",
        "city": "NYC",
        "zip_code": "10001"
    }
}

user = User(**user_data)
json_str = user.model_dump_json()

Elixir Ecto Schemas

# Elixir: Ecto schemas with changesets
defmodule MyApp.Address do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :street, :string
    field :city, :string
    field :zip_code, :string
  end

  def changeset(address, attrs) do
    address
    |> cast(attrs, [:street, :city, :zip_code])
    |> validate_required([:street, :city, :zip_code])
    |> validate_format(:zip_code, ~r/^\d{5}$/)
  end
end

defmodule MyApp.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer
    embeds_one :address, MyApp.Address

    timestamps()  # created_at, updated_at
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :age])
    |> cast_embed(:address)
    |> validate_required([:name, :email, :age])
    |> validate_length(:name, min: 1, max: 100)
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than_or_equal_to: 0, less_than_or_equal_to: 150)
    |> validate_adult()
  end

  defp validate_adult(changeset) do
    validate_change(changeset, :age, fn :age, age ->
      if age < 18 do
        [age: "must be 18 or older"]
      else
        []
      end
    end)
  end
end

# Usage
user_attrs = %{
  "name" => "Alice",
  "email" => "alice@example.com",
  "age" => 25,
  "address" => %{
    "street" => "123 Main St",
    "city" => "NYC",
    "zip_code" => "10001"
  }
}

changeset = MyApp.User.changeset(%MyApp.User{}, user_attrs)

case changeset do
  %{valid?: true} ->
    # Insert into database
    Repo.insert(changeset)

  %{valid?: false} ->
    # Get errors
    errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
end

# JSON encoding (with Jason)
user = %MyApp.User{name: "Alice", email: "alice@example.com", age: 25}
json = Jason.encode!(user)

JSON Serialization Without Ecto

# Elixir: Plain structs with Jason
defmodule MyApp.User do
  @derive {Jason.Encoder, only: [:name, :email, :age]}
  defstruct [:name, :email, :age, :internal_field]
end

user = %MyApp.User{name: "Alice", email: "alice@example.com", age: 25}
Jason.encode!(user)
# => {"name":"Alice","email":"alice@example.com","age":25}

# Custom encoding
defimpl Jason.Encoder, for: MyApp.User do
  def encode(user, opts) do
    Jason.Encode.map(%{
      name: user.name,
      email: user.email,
      age: user.age,
      is_adult: user.age >= 18
    }, opts)
  end
end

Serialization Comparison

| Python | Elixir | Notes | |--------|--------|-------| | Pydantic BaseModel | Ecto schema + changeset | Validation via changesets | | @field_validator | validate_change/3 | Custom validation | | Field(pattern=...) | validate_format/3 | Regex validation | | model_dump() | Jason.encode!() | JSON serialization | | model_validate() | changeset.valid? | Validation check | | EmailStr type | validate_format(:email, ~r/@/) | Email validation | | Dataclass field(default_factory=...) | Ecto timestamps() | Auto fields |


Pillar 7: Build System and Dependencies

Python Package Management

# pyproject.toml (Poetry)
[tool.poetry]
name = "my-app"
version = "0.1.0"
description = "My Python application"
authors = ["Your Name <you@example.com>"]

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.104.0"
pydantic = "^2.5.0"
sqlalchemy = "^2.0.0"
requests = "^2.31.0"

[tool.poetry.dev-dependencies]
pytest = "^7.4.0"
black = "^23.11.0"
mypy = "^1.7.0"

# requirements.txt (pip)
fastapi==0.104.0
pydantic==2.5.0
sqlalchemy==2.0.0
requests==2.31.0

# setup.py
from setuptools import setup, find_packages

setup(
    name="my-app",
    version="0.1.0",
    packages=find_packages(),
    install_requires=[
        "fastapi>=0.104.0",
        "pydantic>=2.5.0",
    ],
)

Elixir Mix Configuration

# mix.exs
defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.15",
      start_permanent: Mix.env() == :prod,
      deps: deps(),

      # Aliases for common tasks
      aliases: aliases()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger],
      mod: {MyApp.Application, []}
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:phoenix, "~> 1.7.0"},
      {:ecto_sql, "~> 3.10"},
      {:postgrex, ">= 0.0.0"},
      {:jason, "~> 1.4"},
      {:httpoison, "~> 2.2"},

      # Dev/test dependencies
      {:ex_doc, "~> 0.30", only: :dev, runtime: false},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev], runtime: false}
    ]
  end

  defp aliases do
    [
      setup: ["deps.get", "ecto.setup"],
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
      "ecto.reset": ["ecto.drop", "ecto.setup"],
      test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
    ]
  end
end

Dependency Management Comparison

| Python | Elixir | Notes | |--------|--------|-------| | pip install package | mix deps.get | Install dependencies | | poetry add package | Edit mix.exs, run mix deps.get | Add dependency | | requirements.txt | mix.lock | Lock file | | pyproject.toml | mix.exs | Project config | | python -m venv | N/A (not needed) | No virtual environments | | pip freeze | mix deps.tree | Show dependency tree | | PyPI | Hex.pm | Package registry | | python setup.py install | mix compile | Build project | | python -m my_app | mix run or iex -S mix | Run application |

Common Package Mappings

| Python Package | Elixir Package | Purpose | |----------------|----------------|---------| | requests | httpoison or req | HTTP client | | flask / fastapi | phoenix | Web framework | | sqlalchemy | ecto | Database ORM | | pydantic | ecto changesets | Validation | | pytest | ex_unit | Testing | | celery | GenServer + Task | Background jobs | | redis-py | redix | Redis client | | psycopg2 | postgrex | PostgreSQL driver | | black | mix format | Code formatter | | mypy | dialyzer | Type checking |


Pillar 8: Testing Strategy

Python pytest

# tests/test_users.py
import pytest
from my_app.services import UserService
from my_app.models import User

@pytest.fixture
def user_service():
    return UserService()

@pytest.fixture
def sample_user():
    return User(name="Alice", email="alice@example.com", age=25)

class TestUserService:
    def test_create_user(self, user_service):
        user = user_service.create_user("Bob", "bob@example.com")
        assert user.name == "Bob"
        assert user.email == "bob@example.com"

    def test_get_user_not_found(self, user_service):
        with pytest.raises(UserNotFoundError):
            user_service.get_user(999)

    @pytest.mark.parametrize("age,expected", [
        (17, False),
        (18, True),
        (25, True),
    ])
    def test_is_adult(self, age, expected):
        user = User(name="Test", email="test@example.com", age=age)
        assert user.is_adult() == expected

Elixir ExUnit

# test/my_app/services/user_service_test.exs
defmodule MyApp.Services.UserServiceTest do
  use ExUnit.Case, async: true

  alias MyApp.Services.UserService
  alias MyApp.Models.User

  # Setup runs before each test
  setup do
    user = %User{name: "Alice", email: "alice@example.com", age: 25}
    {:ok, user: user}
  end

  describe "create_user/2" do
    test "creates user with valid data" do
      user = UserService.create_user("Bob", "bob@example.com")
      assert user.name == "Bob"
      assert user.email == "bob@example.com"
    end

    test "returns error with invalid data" do
      assert {:error, _} = UserService.create_user("", "invalid")
    end
  end

  describe "get_user/1" do
    test "returns error when user not found" do
      assert {:error, :not_found} = UserService.get_user(999)
    end
  end

  # Doctest (tests in documentation)
  doctest MyApp.Services.UserService
end

# test/support/factory.ex (test data factory)
defmodule MyApp.Factory do
  def user_attrs(attrs \\ %{}) do
    Map.merge(%{
      name: "Test User",
      email: "test@example.com",
      age: 25
    }, attrs)
  end

  def build(:user, attrs \\ %{}) do
    struct(MyApp.User, user_attrs(attrs))
  end
end

Property-Based Testing

# Python: Hypothesis
from hypothesis import given, strategies as st

@given(st.integers(min_value=0, max_value=150))
def test_age_validation(age):
    user = User(name="Test", email="test@example.com", age=age)
    assert user.age >= 0
    assert user.age <= 150
# Elixir: StreamData
defmodule MyApp.UserPropertyTest do
  use ExUnit.Case
  use ExUnitProperties

  property "age is always within valid range" do
    check all age <- integer(0..150),
              name <- string(:alphanumeric, min_length: 1),
              email <- string(:alphanumeric, min_length: 3) do
      user = %MyApp.User{name: name, email: email <> "@test.com", age: age}
      assert user.age >= 0
      assert user.age <= 150
    end
  end
end

Testing Comparison

| Python | Elixir | Notes | |--------|--------|-------| | @pytest.fixture | setup/1 callback | Test setup | | assert x == y | assert x == y | Same syntax! | | pytest.raises(Error) | assert_raise Error, fn -> ... end | Exception testing | | @pytest.mark.parametrize | Multiple test functions or generators | Parameterized tests | | unittest.mock.patch | Mox library | Mocking | | Hypothesis | StreamData | Property-based testing | | pytest -k test_name | mix test test/path/file_test.exs:10 | Run specific test | | pytest --cov | mix test --cover | Code coverage |


Pillar 9: Dev Workflow and REPL

Python REPL and Development

# Python REPL
$ python
>>> from my_app.models import User
>>> user = User(name="Alice", email="alice@example.com", age=25)
>>> user.name
'Alice'
>>> dir(user)  # Introspection
[...list of attributes...]

# IPython for better REPL
$ ipython
In [1]: from my_app.services import UserService
In [2]: svc = UserService()
In [3]: svc.  # Tab completion

# Python scripts and entry points
# setup.py
entry_points={
    'console_scripts': [
        'my-app=my_app.cli:main',
    ],
}

# Running scripts
$ python -m my_appcli.migrate

Elixir IEx and Development

# Elixir IEx (Interactive Elixir)
$ iex -S mix
Erlang/OTP 26 [erts-14.1.1] ...

iex(1)> alias MyApp.Models.User
MyApp.Models.User

iex(2)> user = %User{name: "Alice", email: "alice@example.com", age: 25}
%MyApp.Models.User{name: "Alice", email: "alice@example.com", age: 25}

iex(3)> user.name
"Alice"

# IEx helpers
iex(4)> h Enum.map  # Documentation
iex(5)> i user      # Introspection
iex(6)> exports User  # List exported functions

# Recompile code without restarting IEx
iex(7)> recompile()

# IEx.pry for breakpoints
defmodule MyApp.Debug do
  def example(x) do
    require IEx
    IEx.pry()  # Breakpoint - drops into IEx
    x * 2
  end
end

Mix Tasks (CLI Scripts)

# lib/mix/tasks/migrate_users.ex
defmodule Mix.Tasks.MigrateUsers do
  use Mix.Task

  @shortdoc "Migrates users from old format to new"
  def run(args) do
    # Start the application
    Mix.Task.run("app.start")

    # Parse args
    {opts, _, _} = OptionParser.parse(args, switches: [dry_run: :boolean])

    if opts[:dry_run] do
      IO.puts("DRY RUN: No changes will be made")
    end

    # Run migration
    MyApp.UserMigration.run(opts)
  end
end

# Usage
$ mix migrate_users
$ mix migrate_users --dry-run

Hot Code Reloading (Production!)

# Elixir: Hot code reload in production
# 1. Build new release
$ mix release

# 2. Deploy new version to running system
$ bin/my_app upgrade 0.2.0

# The system upgrades WITHOUT downtime!
# Running processes automatically migrate to new code

Dev Workflow Comparison

| Python | Elixir | Notes | |--------|--------|-------| | python REPL | iex REPL | Interactive shell | | ipython | iex (built-in rich features) | Enhanced REPL | | dir(obj) | i(obj) or exports Module | Introspection | | help(func) | h func | Documentation | | Import module, test | IEx + recompile() | Fast feedback loop | | python -m module | mix run -e "Module.func()" | Run one-off code | | if __name__ == "__main__": | Mix tasks | CLI entry points | | Restart process for code changes | recompile() in IEx | Hot reload | | Blue-green deploy | mix release upgrade | Zero-downtime deploy | | Print debugging | IO.inspect() with labels | Debugging | | pdb.set_trace() | IEx.pry() | Breakpoints |


Pillar 10: FFI and Interoperability

When to Use FFI for Gradual Migration

Python → Elixir migrations can be incremental using FFI:

  1. Keep Python code that's working well (especially data science, ML)
  2. Migrate critical services to Elixir (concurrency, fault tolerance)
  3. Use Ports or NIFs to call Python from Elixir during transition

Python C Extensions → Elixir NIFs/Ports

# Python: C extension or ctypes
from ctypes import CDLL, c_int

lib = CDLL('./libmylib.so')
result = lib.my_function(c_int(42))
# Elixir: NIF (Native Implemented Function)
defmodule MyApp.NIF do
  @on_load :load_nif

  def load_nif do
    :erlang.load_nif('./priv/mylib', 0)
  end

  # Stub - replaced by NIF implementation
  def my_function(_arg), do: raise "NIF not loaded"
end

# Usage
MyApp.NIF.my_function(42)

Calling Python from Elixir (Ports)

# Elixir: Call Python script via Port
defmodule MyApp.PythonBridge do
  def call_python(script, args) do
    port = Port.open({:spawn, "python3 #{script}"}, [:binary, :exit_status])

    send(port, {self(), {:command, Jason.encode!(args)}})

    receive do
      {^port, {:data, data}} ->
        Jason.decode!(data)

      {^port, {:exit_status, status}} when status != 0 ->
        {:error, :python_error}
    after
      5000 -> {:error, :timeout}
    end
  end
end

# Python script: my_script.py
import sys
import json

def main():
    args = json.loads(sys.stdin.read())
    result = process(args)
    print(json.dumps(result))

if __name__ == "__main__":
    main()

# Usage
MyApp.PythonBridge.call_python("my_script.py", %{data: [1, 2, 3]})

Elixir ↔ Rust via Rustler (Recommended for Performance)

Instead of calling Python C extensions, rewrite in Rust and use Rustler:

# mix.exs
defp deps do
  [
    {:rustler, "~> 0.30.0"}
  ]
end

# lib/my_app/native.ex
defmodule MyApp.Native do
  use Rustler, otp_app: :my_app, crate: "mynative"

  # Stubs - replaced by Rust implementation
  def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
  def process_data(_data), do: :erlang.nif_error(:nif_not_loaded)
end
// native/mynative/src/lib.rs
#[rustler::nif]
fn add(a: i64, b: i64) -> i64 {
    a + b
}

#[rustler::nif]
fn process_data(data: Vec<i64>) -> Vec<i64> {
    data.iter().map(|x| x * 2).collect()
}

rustler::init!("Elixir.MyApp.Native", [add, process_data]);

Gradual Migration Strategy with FFI

┌─────────────────────────────────────────────────────────────┐
│              GRADUAL PYTHON → ELIXIR MIGRATION              │
├─────────────────────────────────────────────────────────────┤
│  Phase 1: IDENTIFY BOUNDARIES                                │
│  • API layer: Migrate to Phoenix                             │
│  • Worker pools: Migrate to GenServer + Supervisor           │
│  • Keep: Data science, ML models in Python                   │
├─────────────────────────────────────────────────────────────┤
│  Phase 2: BUILD BRIDGE                                       │
│  • Expose Python as HTTP service or Port                     │
│  • Elixir calls Python for complex computation               │
│  • Test integration thoroughly                               │
├─────────────────────────────────────────────────────────────┤
│  Phase 3: MIGRATE INCREMENTALLY                              │
│  • Convert one module at a time                              │
│  • Run both Python and Elixir in parallel                    │
│  • Verify equivalence before switching                       │
├─────────────────────────────────────────────────────────────┤
│  Phase 4: OPTIMIZE                                           │
│  • Rewrite critical Python code in Rust (via Rustler)        │
│  • Remove Python dependencies as Elixir versions stabilize   │
│  • Keep Python for truly Python-specific tasks (ML, etc.)    │
└─────────────────────────────────────────────────────────────┘

FFI Comparison

| Python Approach | Elixir Approach | Use Case | |-----------------|-----------------|----------| | ctypes | NIF (via Rustler preferred) | Call C/Rust code | | C extension | Rustler (Rust NIF) | Performance-critical code | | subprocess.run() | Port | Call external programs | | Shared library | NIF or Port | Reuse existing C code | | multiprocessing | BEAM distribution | Cross-node communication |


Idiom Translation Patterns

Pattern 1: List Comprehensions

# Python
squares = [x**2 for x in range(10)]
evens = [x for x in range(10) if x % 2 == 0]
matrix = [[i+j for j in range(3)] for i in range(3)]
# Elixir
squares = for x <- 0..9, do: x * x
evens = for x <- 0..9, rem(x, 2) == 0, do: x
matrix = for i <- 0..2, do: (for j <- 0..2, do: i + j)

# Or with Enum
squares = Enum.map(0..9, &(&1 * &1))
evens = Enum.filter(0..9, &(rem(&1, 2) == 0))

Pattern 2: Dictionary Comprehensions

# Python
word_lengths = {word: len(word) for word in ["hello", "world"]}
inverted = {v: k for k, v in original.items()}
# Elixir
word_lengths = for word <- ["hello", "world"], into: %{}, do: {word, String.length(word)}

# Or with Enum
word_lengths = Map.new(["hello", "world"], fn word -> {word, String.length(word)} end)

inverted = Map.new(original, fn {k, v} -> {v, k} end)

Pattern 3: Context Managers (with statement)

# Python
with open("file.txt", "r") as f:
    content = f.read()
# File automatically closed

# Database transaction
with database.transaction():
    database.insert(record)
    database.update(other)
# Auto commit or rollback
# Elixir: File is auto-closed
{:ok, content} = File.read("file.txt")

# Ecto transaction
Repo.transaction(fn ->
  Repo.insert(record)
  Repo.update(other)
end)
# Auto commit or rollback on error

Pattern 4: Property Decorators

# Python
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

    @property
    def diameter(self):
        return self._radius * 2
# Elixir: Use structs with functions
defmodule Circle do
  defstruct [:radius]

  def area(%Circle{radius: r}), do: 3.14159 * r * r
  def diameter(%Circle{radius: r}), do: r * 2
end

# Usage
circle = %Circle{radius: 5}
Circle.area(circle)
Circle.diameter(circle)

Pattern 5: Iteration with Index

# Python
for i, value in enumerate(["a", "b", "c"]):
    print(f"{i}: {value}")

for name, age in zip(names, ages):
    print(f"{name} is {age}")
# Elixir
["a", "b", "c"]
|> Enum.with_index()
|> Enum.each(fn {value, i} -> IO.puts("#{i}: #{value}") end)

Enum.zip(names, ages)
|> Enum.each(fn {name, age} -> IO.puts("#{name} is #{age}") end)

Pattern 6: Default Arguments and Keyword Arguments

# Python
def greet(name, greeting="Hello", punctuation="!"):
    return f"{greeting}, {name}{punctuation}"

greet("Alice")
greet("Bob", greeting="Hi")
greet("Charlie", punctuation=".")
# Elixir
defmodule Greeter do
  def greet(name, opts \\ []) do
    greeting = Keyword.get(opts, :greeting, "Hello")
    punctuation = Keyword.get(opts, :punctuation, "!")
    "#{greeting}, #{name}#{punctuation}"
  end
end

Greeter.greet("Alice")
Greeter.greet("Bob", greeting: "Hi")
Greeter.greet("Charlie", punctuation: ".")

Pattern 7: Error Propagation

# Python
def process_data(file_path: str) -> dict:
    try:
        data = read_file(file_path)
        parsed = parse_data(data)
        validated = validate_data(parsed)
        return transform_data(validated)
    except FileNotFoundError:
        raise DataProcessingError("File not found")
    except ParseError as e:
        raise DataProcessingError(f"Parse failed: {e}")
# Elixir: with construct
defmodule DataProcessor do
  def process_data(file_path) do
    with {:ok, data} <- read_file(file_path),
         {:ok, parsed} <- parse_data(data),
         {:ok, validated} <- validate_data(parsed),
         {:ok, transformed} <- transform_data(validated) do
      {:ok, transformed}
    else
      {:error, :enoent} ->
        {:error, :file_not_found}

      {:error, {:parse_error, reason}} ->
        {:error, {:parse_failed, reason}}

      error ->
        {:error, {:processing_failed, error}}
    end
  end
end

Pattern 8: Class Methods and Static Methods

# Python
class User:
    @classmethod
    def from_dict(cls, data: dict) -> "User":
        return cls(name=data['name'], email=data['email'])

    @staticmethod
    def validate_email(email: str) -> bool:
        return '@' in email
# Elixir: All functions in modules
defmodule User do
  defstruct [:name, :email]

  # Factory function (like @classmethod)
  def from_map(data) do
    %User{
      name: Map.get(data, "name"),
      email: Map.get(data, "email")
    }
  end

  # Module function (like @staticmethod)
  def validate_email(email) do
    String.contains?(email, "@")
  end
end

Common Pitfalls

1. Expecting Mutable State

# Python: Mutable data
users = []
users.append({"name": "Alice"})
users[0]["age"] = 25  # Mutates in place
# ❌ Can't mutate in Elixir
users = []
users = users ++ [%{name: "Alice"}]
# users = [%{name: "Alice"}]

# Can't do: users[0]["age"] = 25

# ✓ Create new version
users = List.update_at(users, 0, fn user -> Map.put(user, :age, 25) end)

2. Using Classes for Everything

# Python: OOP pattern
class UserRepository:
    def __init__(self, db):
        self.db = db

    def find(self, id):
        return self.db.query(id)
# ❌ Don't translate classes directly
defmodule UserRepository do
  defstruct [:db]

  def new(db), do: %UserRepository{db: db}
  def find(%UserRepository{db: db}, id), do: db.query(id)
end

# ✓ Use modules with dependency injection
defmodule UserRepository do
  def find(db, id), do: db.query(id)
end

# Or use GenServer for stateful repositories
defmodule UserRepository do
  use GenServer

  def start_link(db), do: GenServer.start_link(__MODULE__, db, name: __MODULE__)
  def find(id), do: GenServer.call(__MODULE__, {:find, id})

  def handle_call({:find, id}, _from, db) do
    {:reply, db.query(id), db}
  end
end

3. Blocking Operations in GenServer

# ❌ Blocking GenServer with slow operation
defmodule SlowWorker do
  use GenServer

  def handle_call(:work, _from, state) do
    # Blocks GenServer for 10 seconds!
    Process.sleep(10_000)
    {:reply, :done, state}
  end
end

# ✓ Spawn task for slow work
defmodule FastWorker do
  use GenServer

  def handle_call(:work, from, state) do
    Task.start(fn ->
      Process.sleep(10_000)
      GenServer.reply(from, :done)
    end)
    {:noreply, state}
  end
end

4. Forgetting to Start Supervision Tree

# Python: Direct instantiation
worker = Worker()
worker.start()
# ❌ Spawning without supervision
pid = spawn(fn -> Worker.run() end)

# ✓ Use supervision
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      {MyApp.Worker, []}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

5. Not Leveraging Pattern Matching

# Python: Conditional logic
def handle_response(response):
    if response['status'] == 'ok':
        return response['data']
    elif response['status'] == 'error':
        raise Exception(response['message'])
    else:
        return None
# ❌ Transliterated conditionally
def handle_response(response) do
  cond do
    response.status == :ok -> response.data
    response.status == :error -> raise response.message
    true -> nil
  end
end

# ✓ Use pattern matching
def handle_response({:ok, data}), do: data
def handle_response({:error, message}), do: raise message
def handle_response(_), do: nil

Complete Example: Python → Elixir

Python: Simple User Service

# models.py
from dataclasses import dataclass
from typing import Optional

@dataclass
class User:
    id: int
    name: str
    email: str
    active: bool = True

# repository.py
from typing import Optional, List

class UserRepository:
    def __init__(self):
        self.users: dict[int, User] = {}
        self.next_id = 1

    def create(self, name: str, email: str) -> User:
        user = User(id=self.next_id, name=name, email=email)
        self.users[self.next_id] = user
        self.next_id += 1
        return user

    def find(self, user_id: int) -> Optional[User]:
        return self.users.get(user_id)

    def all(self) -> List[User]:
        return list(self.users.values())

# service.py
class UserService:
    def __init__(self, repository: UserRepository):
        self.repository = repository

    def register_user(self, name: str, email: str) -> User:
        if not self.validate_email(email):
            raise ValueError("Invalid email")
        return self.repository.create(name, email)

    def get_user(self, user_id: int) -> User:
        user = self.repository.find(user_id)
        if user is None:
            raise UserNotFoundError(f"User {user_id} not found")
        return user

    @staticmethod
    def validate_email(email: str) -> bool:
        return '@' in email

Elixir: Equivalent Service

# lib/my_app/models/user.ex
defmodule MyApp.Models.User do
  @enforce_keys [:id, :name, :email]
  defstruct [:id, :name, :email, active: true]

  @type t :: %__MODULE__{
    id: pos_integer(),
    name: String.t(),
    email: String.t(),
    active: boolean()
  }
end

# lib/my_app/repositories/user_repository.ex
defmodule MyApp.Repositories.UserRepository do
  use GenServer
  alias MyApp.Models.User

  # Client API
  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, :ok, Keyword.put_new(opts, :name, __MODULE__))
  end

  def create(name, email) do
    GenServer.call(__MODULE__, {:create, name, email})
  end

  def find(user_id) do
    GenServer.call(__MODULE__, {:find, user_id})
  end

  def all do
    GenServer.call(__MODULE__, :all)
  end

  # Server callbacks
  @impl true
  def init(:ok) do
    {:ok, %{users: %{}, next_id: 1}}
  end

  @impl true
  def handle_call({:create, name, email}, _from, %{users: users, next_id: id} = state) do
    user = %User{id: id, name: name, email: email}
    new_state = %{users: Map.put(users, id, user), next_id: id + 1}
    {:reply, {:ok, user}, new_state}
  end

  @impl true
  def handle_call({:find, user_id}, _from, %{users: users} = state) do
    result = case Map.get(users, user_id) do
      nil -> {:error, :not_found}
      user -> {:ok, user}
    end
    {:reply, result, state}
  end

  @impl true
  def handle_call(:all, _from, %{users: users} = state) do
    {:reply, Map.values(users), state}
  end
end

# lib/my_app/services/user_service.ex
defmodule MyApp.Services.UserService do
  alias MyApp.Repositories.UserRepository
  alias MyApp.Models.User

  @spec register_user(String.t(), String.t()) :: {:ok, User.t()} | {:error, atom()}
  def register_user(name, email) do
    with :ok <- validate_email(email),
         {:ok, user} <- UserRepository.create(name, email) do
      {:ok, user}
    else
      {:error, :invalid_email} -> {:error, :invalid_email}
      error -> error
    end
  end

  @spec get_user(pos_integer()) :: {:ok, User.t()} | {:error, atom()}
  def get_user(user_id) do
    UserRepository.find(user_id)
  end

  @spec validate_email(String.t()) :: :ok | {:error, :invalid_email}
  defp validate_email(email) do
    if String.contains?(email, "@") do
      :ok
    else
      {:error, :invalid_email}
    end
  end
end

# lib/my_app/application.ex
defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      MyApp.Repositories.UserRepository
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Tooling and Resources

Code Formatters

| Python | Elixir | |--------|--------| | black | mix format (built-in) | | Configuration in pyproject.toml | Configuration in .formatter.exs |

Static Analysis

| Python | Elixir | |--------|--------| | mypy (type checking) | dialyzer (type checking) | | pylint | credo (code quality) | | flake8 | mix compile --warnings-as-errors |

Documentation

| Python | Elixir | |--------|--------| | Docstrings | @moduledoc and @doc | | Sphinx | ExDoc (built-in) | | """triple quotes""" | """triple quotes""" (same!) |

REPL/Interactive Development

| Python | Elixir | |--------|--------| | python or ipython | iex | | import module; reload(module) | recompile() | | dir(obj) | i(obj) | | help(func) | h func |


References

Skills That Extend This Skill

  • convert-python-rust - Python → Rust (different concurrency model)
  • convert-python-golang - Python → Go (simpler than Elixir)

Related Skills

  • lang-python-dev - Python fundamentals
  • lang-elixir-dev - Elixir fundamentals
  • patterns-concurrency-dev - Cross-language concurrency patterns
  • patterns-serialization-dev - Cross-language serialization
  • meta-convert-dev - General conversion methodology

External Resources