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:
- Identify concurrency patterns - threading, asyncio, multiprocessing
- Map class hierarchies - OOP → functional decomposition
- Note mutable state - Global variables, class attributes
- Find decorators - @property, @staticmethod, custom decorators
- Catalog dependencies - requests, pydantic, sqlalchemy, etc.
- Error handling style - Exception hierarchy, custom errors
- 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:
- Module structure first - Create Elixir module hierarchy
- Data types - Pydantic/dataclasses → Ecto schemas or structs
- Pure functions - Easiest conversions, no side effects
- Stateful components - Classes → GenServers or Agents
- Concurrency - async/threading → Task/GenServer/Supervisor
- Error handling - Exceptions → tagged tuples and pattern matching
- Tests - pytest → ExUnit with doctests
Golden Rule: Write idiomatic Elixir, not "Python in Elixir syntax"
4. VALIDATE Phase
- Functional equivalence - Same inputs → same outputs
- Property-based tests - Use StreamData for edge cases
- Concurrent behavior - Test supervision trees, process isolation
- Performance - Benchmark against Python (expect gains in concurrency)
- 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_app.scripts.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:
- Keep Python code that's working well (especially data science, ML)
- Migrate critical services to Elixir (concurrency, fault tolerance)
- 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