# Convert Python Elm

> Convert Python code to idiomatic Elm. Use when migrating Python backends to Elm frontends, translating Python logic to type-safe frontend code, or refactoring Python codebases into functional-first Elm applications. Extends meta-convert-dev with Python-to-Elm specific patterns focused on The Elm Architecture (TEA).

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

---


# Convert Python to Elm

Convert Python code to idiomatic Elm for type-safe frontend applications. This skill extends `meta-convert-dev` with Python-to-Elm specific type mappings, idiom translations, and patterns for transforming dynamic, imperative Python code into functional, purely-functional Elm code with The Elm Architecture.

## This Skill Extends

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

For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.

## This Skill Adds

- **Type mappings**: Python types → Elm types (dynamic → static, runtime → compile-time)
- **Idiom translations**: Imperative/OOP Python → functional-first Elm
- **Error handling**: try/except → Result/Maybe with pattern matching
- **Concurrency**: Python async/threading → The Elm Architecture (Cmd/Sub)
- **Metaprogramming**: Python decorators/metaclasses → elm-codegen and derivation patterns
- **Architecture translation**: Any Python pattern → The Elm Architecture (TEA)
- **Platform differences**: Backend/CLI Python → Frontend browser-based Elm
- **No runtime exceptions**: All errors handled at compile-time or through types
- **JSON serialization**: Pydantic → Json.Decode/Json.Encode pipelines

## This Skill Does NOT Cover

- General conversion methodology - see `meta-convert-dev`
- Python language fundamentals - see `lang-python-dev`
- Elm language fundamentals - see `lang-elm-dev`
- Reverse conversion (Elm → Python) - not typically needed
- Fable.Python (F# to Python via Fable) - different toolchain

---

## Quick Reference

| Python | Elm | Notes |
|--------|------|-------|
| `int` | `Int` | Elm Int is JavaScript number (53-bit precision) |
| `float` | `Float` | IEEE 754 double precision |
| `bool` | `Bool` | Direct mapping |
| `str` | `String` | UTF-8 in Elm vs Python's unicode |
| `bytes` | - | No direct equivalent; use String or Array Int |
| `list[T]` | `List a` | Immutable linked list |
| `tuple` | `( a, b )` / `( a, b, c )` | Max 3-tuple in Elm |
| `dict[K, V]` | `Dict comparable v` | Key must be comparable |
| `set[T]` | `Set comparable` | Value must be comparable |
| `None` | `Nothing` (in `Maybe a`) | Explicit optional |
| `Union[T, U]` | `type X = A T \| B U` | Discriminated union |
| `Callable` | `a -> b` | Function type |
| `async def` | `Cmd Msg` / `Task error value` | No async/await; effects at edges |
| `@dataclass` | `type alias Record = { }` | Records with structural equality |
| `try/except` | `Result error value` | No exceptions; use Result/Maybe |
| `class` | N/A (use TEA) | No OOP; use Model-Update-View pattern |

## When Converting Code

1. **Analyze source thoroughly** before writing target - understand data flow
2. **Map types first** - create type equivalence table
3. **Identify side effects** - all effects go through Cmd/Sub in Elm
4. **Embrace immutability** - Elm has no mutable state
5. **Adopt The Elm Architecture** - don't write "Python code in Elm syntax"
6. **No runtime exceptions** - compiler catches all errors
7. **JSON handling is explicit** - write decoders/encoders for all data
8. **Test equivalence** - same inputs → same outputs (for pure functions)

---

## Critical Paradigm Shift: Python → Elm

### What Makes This Conversion Different

Python to Elm is not just a syntax translation - it represents a fundamental shift in programming paradigm:

| Aspect | Python | Elm |
|--------|--------|-----|
| **Type System** | Dynamic, runtime | Static, compile-time |
| **Null Safety** | None everywhere, NoneType errors | Maybe type, no null |
| **Error Handling** | Exceptions | Result/Maybe, no exceptions |
| **Mutability** | Mutable by default | Immutable always |
| **Side Effects** | Anywhere | Only through Cmd/Sub at edges |
| **Concurrency** | async/await, threading | N/A - single-threaded with Cmd/Sub |
| **OOP** | Classes, inheritance | N/A - data and functions separate |
| **Runtime** | CPython, PyPy (backend) | JavaScript (browser) |
| **Package Manager** | pip, poetry | elm.json |
| **REPL** | Interactive Python shell | elm repl (limited) |

### The Elm Philosophy

**"If it compiles, it works"** - Elm's compiler is so strict that runtime exceptions are virtually impossible:

- **No null pointer exceptions** - Maybe type makes absence explicit
- **No undefined is not a function** - All types known at compile time
- **No runtime type errors** - Static types prevent mismatches
- **No silent failures** - Result type makes errors explicit

**Making Impossible States Impossible** - Elm encourages modeling your domain such that invalid states cannot be represented:

```python
# Python: Invalid states possible
class User:
    def __init__(self):
        self.loading = False
        self.data = None
        self.error = None

    # Problem: Can have loading=True, data=X, error=Y simultaneously!
```

```elm
-- Elm: Invalid states impossible
type UserState
    = Loading
    | Success User
    | Failure Http.Error

-- Can only be in ONE state at a time
```

---

## The 10 Pillars of Conversion

This skill organizes Python → Elm conversion patterns into 10 pillars based on the `meta-convert-dev` framework:

1. **Module System** - Python packages → Elm modules
2. **Error Handling** - Exceptions → Result/Maybe
3. **Concurrency** - async/await → Cmd/Sub (Elm Architecture)
4. **Metaprogramming** - Decorators/metaclasses → elm-codegen
5. **Zero/Default Values** - None/defaults → Maybe/withDefault
6. **Serialization** - Pydantic → Json.Decode/Json.Encode
7. **Build & Dependencies** - pip/poetry → elm.json
8. **Testing** - pytest → elm-test
9. **Dev Workflow & REPL** - Python REPL → elm repl/reactor
10. **FFI/Interop** - C extensions → Ports (JavaScript interop)

Each pillar is covered in detail below.

---

## Pillar 1: Module System Translation

Python's flexible module system with dynamic imports contrasts with Elm's strict, static module system.

### Module Declaration

**Python:**
```python
# mymodule.py - filename determines module name
# No explicit declaration needed

def public_function():
    pass

def _private_function():
    pass

__all__ = ['public_function']  # Optional export control
```

**Elm:**
```elm
-- MyModule.elm - filename must match module name
module MyModule exposing (publicFunction)

-- Must explicitly declare what's exposed
publicFunction : String -> String
publicFunction input =
    privateFunction input

-- Not exposed, module-private
privateFunction : String -> String
privateFunction input =
    String.toUpper input
```

### Import Patterns

| Python | Elm | Notes |
|--------|------|-------|
| `import json` | `import Json.Decode` | Qualified import |
| `import json as j` | `import Json.Decode as Decode` | Alias |
| `from json import loads` | `import Json.Decode exposing (decodeString)` | Specific items |
| `from json import *` | `import Json.Decode exposing (..)` | All items (discouraged) |
| `import .relative` | - | No relative imports in Elm |

### Package Structure

**Python:**
```
myproject/
  myproject/
    __init__.py       # Package marker
    core.py
    utils/
      __init__.py
      helpers.py
```

**Elm:**
```
myproject/
  src/
    Main.elm          # Entry point
    Core.elm          # No __init__ needed
    Utils/
      Helpers.elm     # Capitalized names
```

### Avoiding Import Cycles

**Python:**
```python
# a.py
from b import B
class A:
    def use_b(self, b: B):
        pass

# b.py
from a import A  # Circular import!
class B:
    def use_a(self, a: A):
        pass

# Solution: Move to separate types.py or use TYPE_CHECKING
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from a import A
```

**Elm:**
```elm
-- Elm PROHIBITS circular imports at compile time

-- Solution: Extract shared types
-- Types.elm
module Types exposing (User, Msg(..))

type alias User = { name : String }
type Msg = UpdateUser User

-- ModuleA.elm
import Types exposing (User, Msg)

-- ModuleB.elm
import Types exposing (User, Msg)
```

### Opaque Types (Encapsulation)

**Python:**
```python
# email.py
class Email:
    def __init__(self, value: str):
        if "@" not in value:
            raise ValueError("Invalid email")
        self._value = value  # Convention: _ = private

    @property
    def value(self) -> str:
        return self._value

# Users can still access _value directly (no true privacy)
```

**Elm:**
```elm
-- Email.elm
module Email exposing (Email, fromString, toString)

-- Opaque type: constructor NOT exposed
type Email = Email String

fromString : String -> Maybe Email
fromString str =
    if String.contains "@" str then
        Just (Email str)
    else
        Nothing

toString : Email -> String
toString (Email str) =
    str

-- Users CANNOT construct Email directly
-- Email "invalid" → Compile error!
```

---

## Pillar 2: Error Handling Translation

Python's exception-based error handling fundamentally differs from Elm's type-based error handling.

### Exception Model → Result/Maybe Types

**Python exceptions:**
- Thrown anywhere
- Propagate up the stack
- Can crash if not caught
- Types not tracked

**Elm Result/Maybe:**
- Explicit in type signature
- Must be handled explicitly
- Cannot crash (compiler enforces handling)
- Types tracked at compile time

### Try/Except → Pattern Matching

**Python:**
```python
def parse_user(json_str: str) -> User:
    try:
        data = json.loads(json_str)
        return User(
            name=data['name'],
            email=data['email']
        )
    except (json.JSONDecodeError, KeyError) as e:
        raise ValueError(f"Failed to parse user: {e}")
    except Exception as e:
        # Unexpected error
        raise
```

**Elm:**
```elm
-- All errors are values, not exceptions
parseUser : String -> Result String User
parseUser jsonStr =
    Decode.decodeString userDecoder jsonStr
        |> Result.mapError (\err -> "Failed to parse user: " ++ Decode.errorToString err)

-- Type signature SHOWS this can fail
-- Compiler FORCES caller to handle Result
```

### None/AttributeError → Maybe

**Python:**
```python
def get_user_email(user_id: int) -> str | None:
    user = database.get(user_id)  # Returns None if not found
    if user is None:
        return None
    return user.email

# Usage:
email = get_user_email(123)
if email is not None:
    send_email(email)
else:
    print("User not found")
```

**Elm:**
```elm
getUserEmail : Int -> Maybe String
getUserEmail userId =
    Dict.get userId database
        |> Maybe.map .email

-- Usage: Explicit handling required
case getUserEmail 123 of
    Just email ->
        sendEmail email

    Nothing ->
        "User not found"

-- Or use Maybe combinators
getUserEmail 123
    |> Maybe.withDefault "no-reply@example.com"
    |> sendEmail
```

### Result Combinators vs Try/Except Chains

**Python:**
```python
def process_order(order_id: int) -> Order:
    try:
        raw_order = fetch_order(order_id)
    except HTTPError as e:
        raise OrderError(f"Failed to fetch: {e}")

    try:
        validated = validate_order(raw_order)
    except ValidationError as e:
        raise OrderError(f"Validation failed: {e}")

    try:
        saved = save_order(validated)
    except DatabaseError as e:
        raise OrderError(f"Save failed: {e}")

    return saved
```

**Elm:**
```elm
-- Railway-Oriented Programming
processOrder : Int -> Task Error Order
processOrder orderId =
    fetchOrder orderId
        |> Task.andThen validateOrder
        |> Task.andThen saveOrder

-- All errors flow through the Result/Task
-- No hidden exceptions
-- Type signature shows Error possibility
```

### Error Types Translation

**Python:**
```python
class AppError(Exception):
    pass

class NotFoundError(AppError):
    def __init__(self, resource: str):
        super().__init__(f"{resource} not found")

class ValidationError(AppError):
    def __init__(self, field: str, message: str):
        super().__init__(f"{field}: {message}")

# Usage:
raise NotFoundError("User")
```

**Elm:**
```elm
-- Use discriminated unions for errors
type AppError
    = NotFound String
    | ValidationError String String
    | NetworkError Http.Error

-- Usage: Return error as value
findUser : Int -> Result AppError User
findUser id =
    case Dict.get id users of
        Just user ->
            Ok user

        Nothing ->
            Err (NotFound "User")

-- Pattern match to handle
case findUser 123 of
    Ok user ->
        viewUser user

    Err (NotFound resource) ->
        text ("Not found: " ++ resource)

    Err (ValidationError field msg) ->
        text (field ++ ": " ++ msg)

    Err (NetworkError httpErr) ->
        text "Network error"
```

### Validation Patterns

**Python (with Pydantic):**
```python
from pydantic import BaseModel, validator, ValidationError

class User(BaseModel):
    name: str
    email: str
    age: int

    @validator('email')
    def validate_email(cls, v):
        if '@' not in v:
            raise ValueError('Invalid email')
        return v

    @validator('age')
    def validate_age(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v

# Usage:
try:
    user = User(name="Alice", email="alice@example.com", age=30)
except ValidationError as e:
    print(e.errors())
```

**Elm:**
```elm
-- Validation returns Result
type alias User =
    { name : String
    , email : String
    , age : Int
    }

type ValidationError
    = InvalidEmail
    | InvalidAge

validateEmail : String -> Result ValidationError String
validateEmail email =
    if String.contains "@" email then
        Ok email
    else
        Err InvalidEmail

validateAge : Int -> Result ValidationError Int
validateAge age =
    if age >= 0 then
        Ok age
    else
        Err InvalidAge

createUser : String -> String -> Int -> Result ValidationError User
createUser name email age =
    Result.map3 User
        (Ok name)
        (validateEmail email)
        (validateAge age)

-- Usage: Must handle Result
case createUser "Alice" "alice@example.com" 30 of
    Ok user ->
        -- Success
        viewUser user

    Err InvalidEmail ->
        text "Invalid email address"

    Err InvalidAge ->
        text "Age must be positive"
```

---

## Pillar 3: Concurrency Translation

**Critical Note:** Elm has NO concurrency model in the traditional sense. Elm is single-threaded and runs in the browser's event loop. All "async" operations are managed through **The Elm Architecture** via `Cmd` and `Sub`.

### Python Async/Threading → Elm Architecture

**Python async/await:**
```python
import asyncio
import aiohttp

async def fetch_users():
    async with aiohttp.ClientSession() as session:
        async with session.get('https://api.example.com/users') as response:
            return await response.json()

async def main():
    users = await fetch_users()
    print(f"Fetched {len(users)} users")

asyncio.run(main())
```

**Elm with Cmd (no async/await):**
```elm
-- ALL effects happen at the edges via Cmd
type Msg
    = FetchUsers
    | GotUsers (Result Http.Error (List User))

type alias Model =
    { users : List User
    , status : String
    }

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchUsers ->
            ( { model | status = "Loading..." }
            , Http.get
                { url = "https://api.example.com/users"
                , expect = Http.expectJson GotUsers usersDecoder
                }
            )

        GotUsers (Ok users) ->
            ( { model | users = users, status = "Success" }
            , Cmd.none
            )

        GotUsers (Err error) ->
            ( { model | status = "Failed to fetch users" }
            , Cmd.none
            )

-- No "await" - results come back as Msg
```

### Threading → No Threading

**Python threading:**
```python
import threading
import queue

def worker(q):
    while True:
        item = q.get()
        if item is None:
            break
        process(item)
        q.task_done()

q = queue.Queue()
threads = []
for i in range(4):
    t = threading.Thread(target=worker, args=(q,))
    t.start()
    threads.append(t)

# Add work
for item in items:
    q.put(item)

# Wait for completion
q.join()
```

**Elm (no threading):**
```elm
-- Elm is single-threaded
-- All operations happen in sequence in the event loop
-- For "parallel" HTTP requests, use batch:

type Msg
    = FetchAll
    | GotUser1 (Result Http.Error User)
    | GotUser2 (Result Http.Error User)
    | GotUser3 (Result Http.Error User)

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchAll ->
            ( model
            , Cmd.batch
                [ Http.get { url = "/user/1", expect = Http.expectJson GotUser1 userDecoder }
                , Http.get { url = "/user/2", expect = Http.expectJson GotUser2 userDecoder }
                , Http.get { url = "/user/3", expect = Http.expectJson GotUser3 userDecoder }
                ]
            )

        -- Handle each response separately
        GotUser1 result ->
            -- ...

        GotUser2 result ->
            -- ...

        GotUser3 result ->
            -- ...

-- Requests happen "in parallel" (browser manages concurrency)
-- But results are processed sequentially in update function
```

### Background Tasks → Subscriptions

**Python (background polling):**
```python
import asyncio

async def poll_status():
    while True:
        status = await fetch_status()
        print(f"Status: {status}")
        await asyncio.sleep(5)

asyncio.create_task(poll_status())
```

**Elm (subscriptions):**
```elm
-- Subscriptions provide ongoing effects
import Time

type Msg
    = Tick Time.Posix
    | GotStatus (Result Http.Error Status)

subscriptions : Model -> Sub Msg
subscriptions model =
    Time.every 5000 Tick  -- Every 5 seconds

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Tick time ->
            ( model
            , Http.get
                { url = "/status"
                , expect = Http.expectJson GotStatus statusDecoder
                }
            )

        GotStatus (Ok status) ->
            ( { model | status = status }
            , Cmd.none
            )

        GotStatus (Err _) ->
            ( model, Cmd.none )
```

### WebSockets

**Python (websockets library):**
```python
import asyncio
import websockets

async def listen():
    async with websockets.connect('ws://localhost:8000') as ws:
        async for message in ws:
            print(f"Received: {message}")

asyncio.run(listen())
```

**Elm (ports for WebSockets):**
```elm
-- WebSockets via ports (JavaScript interop)
port module Main exposing (..)

-- Outgoing: Send to JavaScript
port sendMessage : String -> Cmd msg

-- Incoming: Receive from JavaScript
port receiveMessage : (String -> msg) -> Sub msg

type Msg
    = Send String
    | Receive String

subscriptions : Model -> Sub Msg
subscriptions model =
    receiveMessage Receive

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Send message ->
            ( model, sendMessage message )

        Receive message ->
            ( { model | messages = message :: model.messages }
            , Cmd.none
            )
```

```javascript
// JavaScript side (ports)
const app = Elm.Main.init({ node: document.getElementById('elm') });

const ws = new WebSocket('ws://localhost:8000');

ws.onmessage = (event) => {
    app.ports.receiveMessage.send(event.data);
};

app.ports.sendMessage.subscribe((message) => {
    ws.send(message);
});
```

### Concurrency Checklist

When converting Python concurrency to Elm:

- [ ] Identify all async operations (HTTP, timers, WebSockets)
- [ ] Model each async result as a Msg variant
- [ ] Use `Cmd` for one-off effects (HTTP requests)
- [ ] Use `Sub` for ongoing effects (timers, WebSocket messages)
- [ ] Use `Cmd.batch` for "parallel" operations
- [ ] Use ports for complex async (WebSockets, file I/O)
- [ ] Remove all threading code (Elm is single-threaded)
- [ ] Remove all mutexes/locks (Elm is immutable)

---

## Pillar 4: Metaprogramming Translation

**Critical Note:** Elm has NO runtime metaprogramming. No decorators, no metaclasses, no `eval()`, no dynamic code generation at runtime.

### Python Metaprogramming → Elm Alternatives

| Python Metaprogramming | Elm Alternative | Notes |
|------------------------|-----------------|-------|
| Decorators | Higher-order functions | Wrap functions at compile time |
| Metaclasses | elm-codegen | Generate code before compilation |
| `@property` | Record fields | Direct field access |
| `__getattr__` | N/A | No dynamic attribute access |
| `type()` / `isinstance()` | Discriminated unions | Pattern matching |
| `eval()` / `exec()` | N/A | No runtime code evaluation |
| Descriptor protocol | N/A | No dynamic behavior |
| Context managers | N/A | Use explicit cleanup in ports |

### Decorators → Higher-Order Functions

**Python decorators:**
```python
import time
from functools import wraps

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

@timing
def slow_function(n):
    time.sleep(n)
    return n * 2

result = slow_function(2)  # Prints timing info
```

**Elm (higher-order functions):**
```elm
-- Elm has no side effects in functions, so timing must go through Cmd
-- But the pattern of wrapping functions works:

type alias Logger msg =
    { log : String -> Cmd msg
    }

withLogging : Logger msg -> (a -> b) -> a -> ( b, Cmd msg )
withLogging logger func input =
    let
        result =
            func input

        logMsg =
            "Function executed with input: " ++ Debug.toString input
    in
    ( result, logger.log logMsg )

-- Usage:
slowFunction : Int -> Int
slowFunction n =
    n * 2

-- In update function:
let
    ( result, cmd ) =
        withLogging logger slowFunction 2
in
( { model | result = result }, cmd )
```

### Code Generation: Python → elm-codegen

**Python (dynamic code generation):**
```python
# Generate classes dynamically
def make_model(fields):
    class DynamicModel:
        def __init__(self, **kwargs):
            for field in fields:
                setattr(self, field, kwargs.get(field))

    return DynamicModel

User = make_model(['name', 'email', 'age'])
user = User(name="Alice", email="alice@example.com", age=30)
```

**Elm (elm-codegen):**
```elm
-- Elm cannot generate code at runtime
-- Use elm-codegen to generate Elm code before compilation

-- codegen/Generate.elm (runs before compilation)
module Generate exposing (main)

import Elm
import Elm.Annotation as Type

main : Program {} () ()
main =
    Elm.generate "src/Generated/Models.elm"
        [ Elm.file [ "Generated", "Models" ]
            [ Elm.alias "User"
                (Type.record
                    [ ( "name", Type.string )
                    , ( "email", Type.string )
                    , ( "age", Type.int )
                    ]
                )
            ]
        ]

-- Run: elm-codegen run
-- Generates src/Generated/Models.elm with type alias User
```

### Property Accessors → Record Fields

**Python:**
```python
class Circle:
    def __init__(self, radius):
        self._radius = radius

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

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

circle = Circle(5)
print(circle.area)  # Computed property
```

**Elm:**
```elm
-- No computed properties
-- Use functions instead

type alias Circle =
    { radius : Float
    }

area : Circle -> Float
area circle =
    pi * circle.radius ^ 2

circumference : Circle -> Float
circumference circle =
    2 * pi * circle.radius

-- Usage:
circle = { radius = 5 }
area circle |> Debug.toString
```

### Type Introspection → Pattern Matching

**Python:**
```python
from typing import Union

def process(value: Union[int, str, list]):
    if isinstance(value, int):
        return value * 2
    elif isinstance(value, str):
        return value.upper()
    elif isinstance(value, list):
        return len(value)
    else:
        raise TypeError(f"Unexpected type: {type(value)}")
```

**Elm:**
```elm
-- Use discriminated unions + pattern matching
type Value
    = IntValue Int
    | StringValue String
    | ListValue (List a)

process : Value -> Int
process value =
    case value of
        IntValue n ->
            n * 2

        StringValue str ->
            String.length str  -- Can't return different types

        ListValue list ->
            List.length list

-- Note: All branches must return same type
-- This is a FEATURE - prevents type confusion
```

### Metaprogramming Checklist

When converting Python metaprogramming to Elm:

- [ ] Replace decorators with higher-order functions
- [ ] Move code generation to elm-codegen (pre-compilation)
- [ ] Replace `@property` with explicit functions
- [ ] Replace `isinstance()` checks with discriminated unions
- [ ] Remove all `eval()`/`exec()` code (no equivalent)
- [ ] Document which metaprogramming patterns have no Elm equivalent
- [ ] Consider if JavaScript interop (ports) is needed

---

## Pillar 5: Zero and Default Values Translation

Python and Elm have fundamentally different approaches to "absence of value."

### None → Maybe

**Python:**
```python
from typing import Optional

def find_user(user_id: int) -> Optional[dict]:
    if user_id in users:
        return users[user_id]
    else:
        return None

# Usage:
user = find_user(123)
if user is not None:
    print(user['name'])
else:
    print("Not found")

# Or with walrus operator:
if (user := find_user(123)) is not None:
    print(user['name'])
```

**Elm:**
```elm
findUser : Int -> Maybe User
findUser userId =
    Dict.get userId users

-- Usage: Must handle Maybe explicitly
case findUser 123 of
    Just user ->
        text user.name

    Nothing ->
        text "Not found"

-- Or with Maybe.withDefault:
findUser 123
    |> Maybe.map .name
    |> Maybe.withDefault "Not found"
    |> text
```

### Default Arguments → Record Update

**Python:**
```python
def create_user(name: str, email: str = "no-reply@example.com", age: int = 0):
    return {
        'name': name,
        'email': email,
        'age': age
    }

user1 = create_user("Alice")
user2 = create_user("Bob", email="bob@example.com")
user3 = create_user("Charlie", age=30)
```

**Elm:**
```elm
-- No default arguments in Elm
-- Pattern 1: Multiple constructor functions

type alias User =
    { name : String
    , email : String
    , age : Int
    }

createUser : String -> String -> Int -> User
createUser name email age =
    { name = name, email = email, age = age }

createUserWithDefaults : String -> User
createUserWithDefaults name =
    { name = name
    , email = "no-reply@example.com"
    , age = 0
    }

-- Pattern 2: Builder pattern with record update
defaultUser : User
defaultUser =
    { name = ""
    , email = "no-reply@example.com"
    , age = 0
    }

user1 = { defaultUser | name = "Alice" }
user2 = { defaultUser | name = "Bob", email = "bob@example.com" }
user3 = { defaultUser | name = "Charlie", age = 30 }

-- Pattern 3: Config record
type alias UserConfig =
    { name : String
    , email : Maybe String
    , age : Maybe Int
    }

createUserFromConfig : UserConfig -> User
createUserFromConfig config =
    { name = config.name
    , email = Maybe.withDefault "no-reply@example.com" config.email
    , age = Maybe.withDefault 0 config.age
    }

user1 = createUserFromConfig { name = "Alice", email = Nothing, age = Nothing }
user2 = createUserFromConfig { name = "Bob", email = Just "bob@example.com", age = Nothing }
```

### Mutable Default Arguments (Gotcha!)

**Python (dangerous pattern):**
```python
def append_to(element, target=[]):  # DANGEROUS!
    target.append(element)
    return target

# Gotcha: Default list is shared!
list1 = append_to(1)  # [1]
list2 = append_to(2)  # [1, 2] ← Shared state!

# Correct pattern:
def append_to(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target
```

**Elm (impossible to have this bug):**
```elm
-- Elm has no mutable defaults or mutable anything
appendTo : a -> List a -> List a
appendTo element target =
    target ++ [ element ]

-- Always creates new list
list1 = appendTo 1 []       -- [1]
list2 = appendTo 2 []       -- [2]
list3 = appendTo 3 list1    -- [1, 3]
-- list1 is still [1] - immutable!
```

### Dictionary Defaults

**Python:**
```python
from collections import defaultdict

# Pattern 1: defaultdict
counts = defaultdict(int)
counts['apples'] += 1  # 0 + 1 = 1

# Pattern 2: dict.get with default
counts = {}
counts['apples'] = counts.get('apples', 0) + 1

# Pattern 3: dict.setdefault
counts = {}
counts.setdefault('apples', 0)
counts['apples'] += 1
```

**Elm:**
```elm
-- Dict.update pattern (functional)
counts : Dict String Int
counts =
    Dict.empty

incrementCount : String -> Dict String Int -> Dict String Int
incrementCount key dict =
    Dict.update key
        (\maybeValue ->
            case maybeValue of
                Just count ->
                    Just (count + 1)

                Nothing ->
                    Just 1
        )
        dict

-- Usage:
newCounts = incrementCount "apples" counts

-- Or helper function:
incrementDefault : comparable -> Int -> Dict comparable Int -> Dict comparable Int
incrementDefault key default dict =
    Dict.insert key
        (Dict.get key dict |> Maybe.withDefault default |> (+) 1)
        dict
```

### Falsy Values

**Python (truthy/falsy):**
```python
# Many values are "falsy" in Python
if not value:  # Could be: None, False, 0, "", [], {}, etc.
    print("Falsy!")

# Explicit checks often better:
if value is None:
    print("Actually None")

if value == "":
    print("Empty string")

if len(value) == 0:
    print("Empty collection")
```

**Elm (no truthiness):**
```elm
-- ONLY Bool values can be used in conditions
-- No truthiness/falsiness concept

-- Check for Maybe:
case maybeValue of
    Just value -> "Has value"
    Nothing -> "No value"

-- Check for empty:
if String.isEmpty str then
    "Empty string"
else
    "Has content"

if List.isEmpty list then
    "Empty list"
else
    "Has items"

-- No implicit boolean conversion
-- This is a COMPILE ERROR:
-- if someString then ...  ← ERROR: String is not Bool
```

---

## Pillar 6: Serialization Translation

Python's Pydantic and JSON handling is runtime-based; Elm's JSON decoders/encoders are compile-time safe.

### Pydantic → Json.Decode

**Python (Pydantic):**
```python
from pydantic import BaseModel
from typing import Optional

class Address(BaseModel):
    street: str
    city: str
    zip_code: Optional[str] = None

class User(BaseModel):
    id: int
    name: str
    email: str
    address: Optional[Address] = None

# Automatic parsing:
json_str = '{"id": 1, "name": "Alice", "email": "alice@example.com"}'
user = User.parse_raw(json_str)  # Automatic!
print(user.name)  # Alice
```

**Elm (Json.Decode):**
```elm
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (required, optional)

type alias Address =
    { street : String
    , city : String
    , zipCode : Maybe String
    }

type alias User =
    { id : Int
    , name : String
    , email : String
    , address : Maybe Address
    }

-- Must write explicit decoder
addressDecoder : Decoder Address
addressDecoder =
    Decode.succeed Address
        |> required "street" Decode.string
        |> required "city" Decode.string
        |> optional "zip_code" (Decode.nullable Decode.string) Nothing

userDecoder : Decoder User
userDecoder =
    Decode.succeed User
        |> required "id" Decode.int
        |> required "name" Decode.string
        |> required "email" Decode.string
        |> optional "address" (Decode.nullable addressDecoder) Nothing

-- Usage:
jsonStr = """{"id": 1, "name": "Alice", "email": "alice@example.com"}"""

case Decode.decodeString userDecoder jsonStr of
    Ok user ->
        text user.name

    Err error ->
        text ("Decode failed: " ++ Decode.errorToString error)
```

### JSON Encoding

**Python:**
```python
import json
from dataclasses import dataclass, asdict

@dataclass
class User:
    id: int
    name: str
    email: str

user = User(id=1, name="Alice", email="alice@example.com")
json_str = json.dumps(asdict(user))
# {"id": 1, "name": "Alice", "email": "alice@example.com"}
```

**Elm:**
```elm
import Json.Encode as Encode

type alias User =
    { id : Int
    , name : String
    , email : String
    }

encodeUser : User -> Encode.Value
encodeUser user =
    Encode.object
        [ ( "id", Encode.int user.id )
        , ( "name", Encode.string user.name )
        , ( "email", Encode.string user.email )
        ]

-- Usage:
user = { id = 1, name = "Alice", email = "alice@example.com" }
jsonStr = Encode.encode 0 (encodeUser user)
-- {"id":1,"name":"Alice","email":"alice@example.com"}
```

### Nested JSON

**Python:**
```python
from pydantic import BaseModel
from typing import List

class Comment(BaseModel):
    author: str
    text: str

class Post(BaseModel):
    title: str
    comments: List[Comment]

json_data = {
    "title": "Hello",
    "comments": [
        {"author": "Alice", "text": "Great!"},
        {"author": "Bob", "text": "Thanks!"}
    ]
}

post = Post(**json_data)
print(post.comments[0].author)  # Alice
```

**Elm:**
```elm
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (required)

type alias Comment =
    { author : String
    , text : String
    }

type alias Post =
    { title : String
    , comments : List Comment
    }

commentDecoder : Decoder Comment
commentDecoder =
    Decode.succeed Comment
        |> required "author" Decode.string
        |> required "text" Decode.string

postDecoder : Decoder Post
postDecoder =
    Decode.succeed Post
        |> required "title" Decode.string
        |> required "comments" (Decode.list commentDecoder)

-- Usage:
jsonStr = """
{
  "title": "Hello",
  "comments": [
    {"author": "Alice", "text": "Great!"},
    {"author": "Bob", "text": "Thanks!"}
  ]
}
"""

case Decode.decodeString postDecoder jsonStr of
    Ok post ->
        case List.head post.comments of
            Just firstComment ->
                text firstComment.author

            Nothing ->
                text "No comments"

    Err error ->
        text ("Decode failed: " ++ Decode.errorToString error)
```

### Handling API Variants

**Python:**
```python
from typing import Union, Literal
from pydantic import BaseModel

class SuccessResponse(BaseModel):
    status: Literal["success"]
    data: dict

class ErrorResponse(BaseModel):
    status: Literal["error"]
    message: str

Response = Union[SuccessResponse, ErrorResponse]

def handle_response(response: Response):
    if isinstance(response, SuccessResponse):
        print(response.data)
    elif isinstance(response, ErrorResponse):
        print(f"Error: {response.message}")
```

**Elm:**
```elm
-- Use discriminated union + oneOf decoder
type ApiResponse
    = Success (Dict String String)
    | Error String

apiResponseDecoder : Decoder ApiResponse
apiResponseDecoder =
    Decode.field "status" Decode.string
        |> Decode.andThen
            (\status ->
                case status of
                    "success" ->
                        Decode.map Success
                            (Decode.field "data" (Decode.dict Decode.string))

                    "error" ->
                        Decode.map Error
                            (Decode.field "message" Decode.string)

                    _ ->
                        Decode.fail ("Unknown status: " ++ status)
            )

-- Usage:
handleResponse : ApiResponse -> String
handleResponse response =
    case response of
        Success data ->
            "Got data: " ++ Debug.toString data

        Error message ->
            "Error: " ++ message
```

### Serialization Checklist

When converting Python serialization to Elm:

- [ ] Replace Pydantic models with Elm type aliases + decoders
- [ ] Write explicit decoder for each type
- [ ] Write explicit encoder for each type (if sending JSON)
- [ ] Use `Json.Decode.Pipeline` for complex decoders
- [ ] Handle optional fields with `optional` from pipeline
- [ ] Use `andThen` for conditional decoding (union types)
- [ ] Test decoders with sample JSON data
- [ ] Document field name mappings (snake_case → camelCase)

---

## Pillar 7: Build & Dependencies Translation

### Package Management

| Python | Elm | Notes |
|--------|-----|-------|
| `requirements.txt` | `elm.json` | Dependency manifest |
| `pyproject.toml` | `elm.json` | Modern Python ≈ Elm manifest |
| `pip install` | `elm install` | Install dependency |
| `pip install -e .` | N/A | No editable installs |
| `venv` / `virtualenv` | N/A | Elm dependencies are per-project |
| `poetry` / `pipenv` | N/A | elm.json is the only tool |

### elm.json Structure

**Python (pyproject.toml):**
```toml
[project]
name = "myapp"
version = "0.1.0"
dependencies = [
    "requests>=2.28.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "black>=23.0.0",
]
```

**Elm (elm.json):**
```json
{
    "type": "application",
    "source-directories": [
        "src"
    ],
    "elm-version": "0.19.1",
    "dependencies": {
        "direct": {
            "elm/core": "1.0.5",
            "elm/html": "1.0.0",
            "elm/http": "2.0.0",
            "elm/json": "1.1.3"
        },
        "indirect": {
            "elm/time": "1.0.0",
            "elm/url": "1.0.0"
        }
    },
    "test-dependencies": {
        "direct": {
            "elm-explorations/test": "1.2.2"
        },
        "indirect": {}
    }
}
```

### Common Dependency Translations

| Python Package | Elm Package | Purpose |
|----------------|-------------|---------|
| `requests` | `elm/http` | HTTP requests |
| `pydantic` | `elm/json` + custom decoders | JSON validation |
| `pytest` | `elm-explorations/test` | Testing |
| `typing` | Built-in type system | Type annotations |
| `dataclasses` | Built-in records | Data structures |
| `enum` | Built-in union types | Enumerations |
| `datetime` | `elm/time` | Date/time handling |
| `re` | `elm/regex` | Regular expressions |
| `asyncio` | `elm/core` (Cmd/Sub) | Async operations |

### Adding Dependencies

**Python:**
```bash
# Add to requirements.txt:
requests>=2.28.0
pydantic>=2.0.0

# Or with poetry:
poetry add requests pydantic
```

**Elm:**
```bash
# Elm installs and updates elm.json automatically:
elm install elm/http
elm install elm/json

# Always uses exact versions (no semver ranges in direct deps)
```

### Build Process

**Python:**
```bash
# No build step (interpreted)
python main.py

# Or package:
python -m build
pip install dist/myapp-0.1.0-py3-none-any.whl
```

**Elm:**
```bash
# Compile to JavaScript:
elm make src/Main.elm --output=main.js

# Compile optimized:
elm make src/Main.elm --optimize --output=main.js

# Development server:
elm reactor  # Opens http://localhost:8000
```

### Project Structure

**Python:**
```
myproject/
  myproject/
    __init__.py
    main.py
    models.py
    api.py
  tests/
    __init__.py
    test_main.py
  pyproject.toml
  README.md
```

**Elm:**
```
myproject/
  src/
    Main.elm
    Types.elm
    Api.elm
  tests/
    MainTest.elm
  elm.json
  README.md
```

---

## Pillar 8: Testing Translation

### pytest → elm-test

**Python (pytest):**
```python
import pytest
from myapp import add, divide

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

def test_divide():
    assert divide(10, 2) == 5

    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

@pytest.mark.parametrize("a,b,expected", [
    (

…(truncated)
