# Convert Python 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.

- Skill: `arustydev/convert-python-elixir` (Agent Skill)
- Install (CLI): `npx skillmds@latest add arustydev/convert-python-elixir`
- Raw SKILL.md: https://api.skillmd.com/api/skills/arustydev/convert-python-elixir/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: aRustyDev (https://skillmd.com/u/arustydev)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/arustydev/convert-python-elixir

---


# 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:**

```python
# 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:**

```elixir
# 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

```python
# 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

```elixir
# 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
# Python imports
from my_package.models import User
from my_package.services import UserService
import my_package.utils as utils
```

```elixir
# 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
# 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
# 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
# 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
# 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

```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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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

```python
# 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

```elixir
# 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

```python
# 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

```elixir
# 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
# 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
# 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
# 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
# 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)

```elixir
# 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
# 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
# Python: C extension or ctypes
from ctypes import CDLL, c_int

lib = CDLL('./libmylib.so')
result = lib.my_function(c_int(42))
```

```elixir
# 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
# 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:

```elixir
# 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
```

```rust
// 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
# 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
# Elixir
squares =

…(truncated)
