docs/getting-started.md

# Getting Started

This guide walks you through using erlang_python to execute Python code from Erlang.

## Installation

Add to your `rebar.config`:

```erlang
{deps, [
    {erlang_python, "2.3.0"}
]}.
```

Or from git:

```erlang
{deps, [
    {erlang_python, {git, "https://github.com/benoitc/erlang-python.git", {branch, "main"}}}
]}.
```

## Starting the Application

```erlang
1> application:ensure_all_started(erlang_python).
{ok, [erlang_python]}
```

The application starts a pool of Python worker processes that handle requests.

## Basic Usage

### Calling Python Functions

```erlang
%% Call math.sqrt(16)
{ok, 4.0} = py:call(math, sqrt, [16]).

%% Call json.dumps with keyword arguments
{ok, Json} = py:call(json, dumps, [#{name => <<"Alice">>}], #{indent => 2}).
```

### Evaluating Expressions

```erlang
%% Simple arithmetic
{ok, 42} = py:eval(<<"21 * 2">>).

%% Using Python built-ins
{ok, 45} = py:eval(<<"sum(range(10))">>).

%% With local variables
{ok, 100} = py:eval(<<"x * y">>, #{x => 10, y => 10}).

%% Note: Python locals aren't accessible in nested scopes (lambda/comprehensions).
%% Use default arguments to capture values:
{ok, [2, 4, 6]} = py:eval(<<"list(map(lambda x, m=multiplier: x * m, items))">>,
                          #{items => [1, 2, 3], multiplier => 2}).
```

### Executing Statements

Use `py:exec/1` to execute Python statements:

```erlang
ok = py:exec(<<"
import random

def roll_dice(sides=6):
    return random.randint(1, sides)
">>).
```

Note: Definitions made with `exec` are local to the worker that executes them.
Subsequent calls may go to different workers. Use [Shared State](#shared-state) to
share data between workers, or [Context Affinity](#context-affinity) to bind to a
dedicated worker.

## Working with Timeouts

All operations support optional timeouts:

```erlang
%% 5 second timeout
{ok, Result} = py:call(mymodule, slow_func, [], #{}, 5000).

%% Timeout error
{error, timeout} = py:eval(<<"sum(range(10**9))">>, #{}, 100).
```

## Async Calls

For non-blocking operations:

```erlang
%% Start async call (returns ref for await)
Ref = py:spawn_call(math, factorial, [1000]).

%% Do other work...

%% Wait for result
{ok, HugeNumber} = py:await(Ref).

%% Fire-and-forget (no result)
ok = py:cast(some_module, log_event, [EventData]).
```

## Streaming from Generators

Python generators can be streamed efficiently:

```erlang
%% Stream a generator expression
{ok, [0,1,4,9,16]} = py:stream_eval(<<"(x**2 for x in range(5))">>).

%% Stream from a generator function (if defined)
{ok, Chunks} = py:stream(mymodule, generate_data, [arg1, arg2]).
```

## Shared State

Python workers don't share namespace state, but you can share data via the
built-in state API:

```erlang
%% Store from Erlang
py:state_store(<<"config">>, #{api_key => <<"secret">>, timeout => 5000}).

%% Read from Python
ok = py:exec(<<"
from erlang import state_get
config = state_get('config')
print(config['api_key'])
">>).
```

### From Python

```python
from erlang import state_set, state_get, state_delete, state_keys
from erlang import state_incr, state_decr

# Key-value storage
state_set('my_key', {'data': [1, 2, 3]})
value = state_get('my_key')

# Atomic counters (thread-safe)
state_incr('requests')       # +1, returns new value
state_incr('requests', 10)   # +10
state_decr('requests')       # -1

# Management
keys = state_keys()
state_delete('my_key')
```

### From Erlang

```erlang
py:state_store(Key, Value).
{ok, Value} = py:state_fetch(Key).
py:state_remove(Key).
Keys = py:state_keys().

%% Atomic counters
1 = py:state_incr(<<"hits">>).
11 = py:state_incr(<<"hits">>, 10).
10 = py:state_decr(<<"hits">>).
```

## Type Conversions

Values are automatically converted between Erlang and Python:

```erlang
%% Numbers
{ok, 42} = py:eval(<<"42">>).           %% int -> integer
{ok, 3.14} = py:eval(<<"3.14">>).       %% float -> float

%% Strings
{ok, <<"hello">>} = py:eval(<<"'hello'">>).  %% str -> binary

%% Collections
{ok, [1,2,3]} = py:eval(<<"[1,2,3]">>).      %% list -> list
{ok, {1,2,3}} = py:eval(<<"(1,2,3)">>).      %% tuple -> tuple
{ok, #{<<"a">> := 1}} = py:eval(<<"{'a': 1}">>).  %% dict -> map

%% Booleans and None
{ok, true} = py:eval(<<"True">>).
{ok, false} = py:eval(<<"False">>).
{ok, none} = py:eval(<<"None">>).
```

## Context Affinity

By default, each call may go to a different context. To preserve Python state across
calls (variables, imports, objects), use an explicit context:

```erlang
%% Get a specific context
Ctx = py:context(1),

%% State persists across calls to the same context
ok = py:exec(Ctx, <<"counter = 0">>),
ok = py:exec(Ctx, <<"counter += 1">>),
{ok, 1} = py:eval(Ctx, <<"counter">>),

ok = py:exec(Ctx, <<"counter += 1">>),
{ok, 2} = py:eval(Ctx, <<"counter">>).
```

Or bind a context to the current process for automatic routing:

```erlang
Ctx = py_context_router:get_context(),
ok = py_context_router:bind_context(Ctx),
try
    ok = py:exec(<<"x = 10">>),
    {ok, 20} = py:eval(<<"x * 2">>)
after
    py_context_router:unbind_context()
end.
```

See [Context Affinity](context-affinity.md) for explicit contexts and advanced usage.

## Virtual Environments

### Automatic Virtual Environment Setup

Use `py:ensure_venv/2,3` to automatically create and activate a virtual environment:

```erlang
%% Create venv and install from requirements.txt
ok = py:ensure_venv("/path/to/myapp/venv", "requirements.txt").

%% Install from pyproject.toml (editable install)
ok = py:ensure_venv("/path/to/venv", "pyproject.toml").

%% With options: extras, custom installer, or force recreate
ok = py:ensure_venv("/path/to/venv", "pyproject.toml", [
    {extras, ["dev", "test"]},   %% Install optional dependencies
    {installer, uv},             %% Use uv instead of pip (faster)
    {python, "/usr/bin/python3.12"}  %% Specific Python version
]).

%% Force recreate even if venv exists
ok = py:ensure_venv("/path/to/venv", "requirements.txt", [force]).
```

### Manual Virtual Environment Activation

```erlang
%% Activate an existing venv
ok = py:activate_venv(<<"/path/to/venv">>).

%% Check current venv
{ok, #{<<"active">> := true, <<"venv_path">> := Path}} = py:venv_info().

%% Deactivate when done
ok = py:deactivate_venv().
```

## Execution Modes and Context Types

### Context Modes

When creating explicit contexts, you can choose different execution modes:

```erlang
%% Worker mode (default, recommended) - main interpreter
%% With free-threaded Python (3.13t+), provides true parallelism automatically
{ok, Ctx} = py_context:new(#{mode => worker}).

%% SHARED_GIL sub-interpreter (Python 3.12+) - isolated namespace
{ok, Ctx} = py_context:new(#{mode => subinterp}).

%% OWN_GIL sub-interpreter (Python 3.14+) - true parallelism
{ok, Ctx} = py_context:new(#{mode => owngil}).
```

| Mode | Python | Description |
|------|--------|-------------|
| `worker` | Any | Main interpreter, shared namespace (default, recommended) |
| `subinterp` | 3.12+ | SHARED_GIL sub-interpreter, isolated namespace |
| `owngil` | 3.14+ | OWN_GIL sub-interpreter, each has own GIL |

**Worker mode is recommended** because it works with any Python version and automatically benefits from free-threaded Python (3.13t+) when available.

**Why OWN_GIL requires Python 3.14+**: C extensions like `_decimal`, `numpy` have global state bugs in sub-interpreters on Python 3.12/3.13. These are fixed in Python 3.14. SHARED_GIL mode works on 3.12+ but some C extensions may have issues.

### Runtime Detection

Check the current execution mode:

```erlang
%% See how Python is being executed
py:execution_mode().
%% => free_threaded | subinterp | multi_executor

%% Check rate limiting status
py_semaphore:max_concurrent().  %% Maximum concurrent calls
py_semaphore:current().         %% Currently executing
```

See [Scalability](scalability.md) for details on execution modes and performance tuning.

## Logging and Tracing

### Python Logging to Erlang

Forward Python `logging` messages to Erlang's `logger`:

```erlang
%% Configure Python logging
ok = py:configure_logging(#{level => info}).

%% Now Python logs appear in Erlang logger
ok = py:exec(<<"
import logging
logging.info('Hello from Python!')
logging.warning('Something needs attention')
">>).
```

### Distributed Tracing

Collect trace spans from Python code:

```erlang
%% Enable tracing
ok = py:enable_tracing().

%% Run traced Python code
ok = py:exec(<<"
import erlang
with erlang.Span('my-operation', key='value'):
    pass  # your code here
">>).

%% Retrieve spans
{ok, Spans} = py:get_traces().

%% Clean up
ok = py:clear_traces().
ok = py:disable_tracing().
```

See [Logging and Tracing](logging.md) for details on span events, decorators, and error handling.

## Using from Elixir

erlang_python works seamlessly with Elixir. The `:py` module can be called directly:

```elixir
# Start the application
{:ok, _} = Application.ensure_all_started(:erlang_python)

# Call Python functions
{:ok, 4.0} = :py.call(:math, :sqrt, [16])

# Evaluate expressions
{:ok, result} = :py.eval("2 + 2")

# With variables
{:ok, 100} = :py.eval("x * y", %{x: 10, y: 10})

# Call with keyword arguments
{:ok, json} = :py.call(:json, :dumps, [%{name: "Elixir"}], %{indent: 2})
```

### Register Elixir Functions for Python

```elixir
# Register an Elixir function
:py.register_function(:factorial, fn [n] ->
  Enum.reduce(1..n, 1, &*/2)
end)

# Call from Python
{:ok, 3628800} = :py.eval("__import__('erlang').call('factorial', 10)")

# Cleanup
:py.unregister_function(:factorial)
```

### Parallel Processing with BEAM

```elixir
# Register parallel map using BEAM processes
:py.register_function(:parallel_map, fn [func_name, items] ->
  parent = self()

  refs = Enum.map(items, fn item ->
    ref = make_ref()
    spawn(fn ->
      result = apply_function(func_name, item)
      send(parent, {ref, result})
    end)
    ref
  end)

  Enum.map(refs, fn ref ->
    receive do
      {^ref, result} -> result
    after
      5000 -> {:error, :timeout}
    end
  end)
end)
```

### Running the Elixir Example

A complete working example is available:

```bash
elixir --erl "-pa _build/default/lib/erlang_python/ebin" examples/elixir_example.exs
```

This demonstrates basic calls, data conversion, callbacks, parallel processing (10x speedup), and AI integration.

## Using the Erlang Event Loop

For async Python code, use the `erlang` module which provides an Erlang-backed asyncio event loop for better performance:

```python
import erlang
import asyncio

async def my_handler():
    # Uses Erlang's erlang:send_after/3 - no Python event loop overhead
    await asyncio.sleep(0.1)  # 100ms
    return "done"

# Run a coroutine with the Erlang event loop
result = erlang.run(my_handler())

# Standard asyncio functions work seamlessly
async def main():
    results = await asyncio.gather(task1(), task2(), task3())

erlang.run(main())
```

See [Asyncio](asyncio.md) for the full API reference.

## Security Considerations

When Python runs inside the Erlang VM, certain operations are blocked for safety:

- **Subprocess operations blocked** - `subprocess.Popen`, `os.fork()`, `os.system()`, etc. would corrupt the Erlang VM
- **Signal handling not supported** - Signal handling should be done at the Erlang level

If you need to run external commands, use Erlang ports (`open_port/2`) instead:

```erlang
%% From Erlang - run a shell command
Port = open_port({spawn, "ls -la"}, [exit_status, binary]),
receive
    {Port, {data, Data}} -> Data;
    {Port, {exit_status, 0}} -> ok
end.
```

See [Security](security.md) for details on blocked operations and recommended alternatives.

## Custom Pool Support

erlang_python lets you create pools on demand to separate CPU-bound and I/O-bound operations:

```erlang
%% Create io pool for I/O-bound operations
{ok, _} = py_context_router:start_pool(io, 10, worker).

%% Register entire modules to io pool
py:register_pool(io, requests).
py:register_pool(io, psycopg2).

%% Or register specific callables
py:register_pool(io, {db, query}).        %% Only db.query goes to io pool

%% Calls automatically route to the right pool
{ok, 4.0} = py:call(math, sqrt, [16]).       %% -> default pool (fast)
{ok, Resp} = py:call(requests, get, [Url]).  %% -> io pool (module registered)
{ok, Rows} = py:call(db, query, [Sql]).      %% -> io pool (callable registered)
```

This prevents slow HTTP requests from blocking quick math operations. See [Pool Support](pools.md) for configuration and advanced usage.

## Zero-Copy Buffers

For streaming data from Erlang to Python (e.g., HTTP request bodies), use `py_buffer`:

```erlang
%% Create buffer for streaming data
{ok, Buf} = py_buffer:new(ContentLength),

%% Write chunks as they arrive
ok = py_buffer:write(Buf, Chunk1),
ok = py_buffer:write(Buf, Chunk2),
ok = py_buffer:close(Buf),

%% Pass buffer to Python handler
py:call(Ctx, myapp, handle, [#{<<"input">> => Buf}]).
```

Python sees it as a file-like object with blocking reads:

```python
def handle(environ):
    body = environ['input'].read()  # Blocks until data ready
    # Or iterate lines
    for line in environ['input']:
        process(line)
```

See [Buffer API](buffer.md) for zero-copy memoryview access and fast substring search.

## Next Steps

- See [Pool Support](pools.md) for separating CPU and I/O operations
- See [Type Conversion](type-conversion.md) for detailed type mapping
- See [Context Affinity](context-affinity.md) for preserving Python state
- See [Streaming](streaming.md) for working with generators
- See [Memory Management](memory.md) for GC and debugging
- See [Scalability](scalability.md) for parallelism and performance
- See [Logging and Tracing](logging.md) for Python logging and distributed tracing
- See [AI Integration](ai-integration.md) for ML/AI examples
- See [Asyncio Event Loop](asyncio.md) for the Erlang-native asyncio implementation with TCP and UDP support
- See [Buffer API](buffer.md) for zero-copy streaming input buffers
- See [Reactor](reactor.md) for FD-based protocol handling
- See [Security](security.md) for sandbox and blocked operations
- See [Distributed Execution](distributed.md) for running Python across Erlang nodes