{"id":44702556,"url":"https://github.com/benoitc/erlang-python","last_synced_at":"2026-04-02T11:40:17.530Z","repository":{"id":338418562,"uuid":"1157804768","full_name":"benoitc/erlang-python","owner":"benoitc","description":"Execute Python from Erlang using dirty NIFs with GIL-aware execution, rate limiting, and free-threading support","archived":false,"fork":false,"pushed_at":"2026-03-29T14:51:20.000Z","size":1991,"stargazers_count":69,"open_issues_count":2,"forks_count":4,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-29T15:22:48.114Z","etag":null,"topics":["ai","beam","elixir","embeddings","erlang","free-threading","interoperability","machine-learning","nif","python"],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/benoitc.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"docs/security.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["benoitc"]}},"created_at":"2026-02-14T10:22:09.000Z","updated_at":"2026-03-29T12:30:14.000Z","dependencies_parsed_at":"2026-02-19T14:01:19.942Z","dependency_job_id":null,"html_url":"https://github.com/benoitc/erlang-python","commit_stats":null,"previous_names":["benoitc/erlang-python"],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/benoitc/erlang-python","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-python","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-python/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-python/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-python/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/benoitc","download_url":"https://codeload.github.com/benoitc/erlang-python/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Ferlang-python/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31305721,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T09:48:21.550Z","status":"ssl_error","status_checked_at":"2026-04-02T09:48:19.196Z","response_time":89,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","beam","elixir","embeddings","erlang","free-threading","interoperability","machine-learning","nif","python"],"created_at":"2026-02-15T10:30:56.602Z","updated_at":"2026-04-02T11:40:17.524Z","avatar_url":"https://github.com/benoitc.png","language":"C","readme":"# erlang_python\n\n[![Hex.pm](https://img.shields.io/hexpm/v/erlang_python.svg)](https://hex.pm/packages/erlang_python)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/erlang_python)\n[![License](https://img.shields.io/hexpm/l/erlang_python.svg)](https://github.com/benoitc/erlang-python/blob/main/LICENSE)\n\n**Combine Python's ML/AI ecosystem with Erlang's concurrency.**\n\nRun Python code from Erlang or Elixir with true parallelism, async/await support,\nand seamless integration. Build AI-powered applications that scale.\n\n## Overview\n\nerlang_python embeds Python into the BEAM VM, letting you call Python functions,\nevaluate expressions, and stream from generators - all without blocking Erlang\nschedulers.\n\n**Parallelism options:**\n- **Worker mode** (default, recommended) - Works with any Python version. With free-threaded Python (3.13t+), provides true parallelism automatically\n- **SHARED_GIL sub-interpreters** (Python 3.12+) - Isolated namespaces, shared GIL (isolation improves in 3.14+)\n- **OWN_GIL sub-interpreters** (Python 3.14+) - Each interpreter has its own GIL, true parallelism\n- **BEAM processes** - Fan out work across lightweight Erlang processes\n\nKey features:\n- **Process-bound environments** - Each Erlang process gets isolated Python state, enabling OTP-supervised Python actors\n- **Async/await** - Call Python async functions, gather results, stream from async generators\n- **Dirty NIF execution** - Python runs on dirty schedulers, never blocking the BEAM\n- **Elixir support** - Works seamlessly from Elixir via the `:py` module\n- **Bidirectional calls** - Python can call back into registered Erlang/Elixir functions\n- **Message passing** - Python can send messages directly to Erlang processes via `erlang.send()`\n- **Type conversion** - Automatic conversion between Erlang and Python types (including PIDs)\n- **Streaming** - Iterate over Python generators chunk-by-chunk\n- **Virtual environments** - Activate venvs for dependency isolation\n- **AI/ML ready** - Examples for embeddings, semantic search, RAG, and LLMs\n- **Logging integration** - Python logging forwarded to Erlang logger\n- **Distributed tracing** - Span-based tracing from Python code\n- **Security sandbox** - Blocks fork/exec operations that would corrupt the VM\n\n## Requirements\n\n- Erlang/OTP 27+\n- Python 3.12+ (3.13+ for free-threading)\n- C compiler (gcc, clang)\n\n## Building\n\n```bash\nrebar3 compile\n```\n\n## Quick Start\n\n### Erlang\n\n```erlang\n%% Start the application\napplication:ensure_all_started(erlang_python).\n\n%% Call a Python function\n{ok, 4.0} = py:call(math, sqrt, [16]).\n\n%% With keyword arguments\n{ok, Json} = py:call(json, dumps, [#{foo =\u003e bar}], #{indent =\u003e 2}).\n\n%% Evaluate an expression\n{ok, 45} = py:eval(\u003c\u003c\"sum(range(10))\"\u003e\u003e).\n\n%% Evaluate with local variables\n{ok, 25} = py:eval(\u003c\u003c\"x * y\"\u003e\u003e, #{x =\u003e 5, y =\u003e 5}).\n\n%% Async calls with await\nRef = py:spawn_call(math, factorial, [100]),\n{ok, Result} = py:await(Ref).\n\n%% Fire-and-forget (no result)\nok = py:cast(erlang, send, [self(), {done, \u003c\u003c\"task1\"\u003e\u003e}]).\n\n%% Streaming from generators\n{ok, [0,1,4,9,16]} = py:stream_eval(\u003c\u003c\"(x**2 for x in range(5))\"\u003e\u003e).\n```\n\n### Elixir\n\n```elixir\n# Start the application\n{:ok, _} = Application.ensure_all_started(:erlang_python)\n\n# Call Python functions\n{:ok, 4.0} = :py.call(:math, :sqrt, [16])\n\n# Evaluate expressions\n{:ok, result} = :py.eval(\"2 + 2\")\n\n# With variables\n{:ok, 100} = :py.eval(\"x * y\", %{x: 10, y: 10})\n\n# Call with keyword arguments\n{:ok, json} = :py.call(:json, :dumps, [%{name: \"Elixir\"}], %{indent: 2})\n```\n\n## Erlang/Elixir Functions Callable from Python\n\nRegister Erlang or Elixir functions that Python code can call back into:\n\n### Erlang\n\n```erlang\n%% Register a function\npy:register_function(my_func, fun([X, Y]) -\u003e X + Y end).\n\n%% Call from Python - native import syntax (recommended)\n{ok, Result} = py:exec(\u003c\u003c\"\nfrom erlang import my_func\nresult = my_func(10, 20)\n\"\u003e\u003e).\n%% Result = 30\n\n%% Or use attribute-style access\n{ok, 30} = py:eval(\u003c\u003c\"erlang.my_func(10, 20)\"\u003e\u003e).\n\n%% Legacy syntax still works\n{ok, 30} = py:eval(\u003c\u003c\"erlang.call('my_func', 10, 20)\"\u003e\u003e).\n\n%% Unregister when done\npy:unregister_function(my_func).\n```\n\n### Elixir\n\n```elixir\n# Register an Elixir function\n:py.register_function(:factorial, fn [n] -\u003e\n  Enum.reduce(1..n, 1, \u0026*/2)\nend)\n\n# Call from Python - native import syntax\n{:ok, 3628800} = :py.exec(\"\"\"\nfrom erlang import factorial\nresult = factorial(10)\n\"\"\")\n\n# Or use attribute-style access\n{:ok, 3628800} = :py.eval(\"erlang.factorial(10)\")\n```\n\n### Python Calling Syntax\n\nFrom Python code, registered Erlang functions can be called in three ways:\n\n```python\n# 1. Import syntax (most Pythonic)\nfrom erlang import my_func\nresult = my_func(10, 20)\n\n# 2. Attribute syntax\nimport erlang\nresult = erlang.my_func(10, 20)\n\n# 3. Explicit call (legacy)\nimport erlang\nresult = erlang.call('my_func', 10, 20)\n```\n\nAll three methods are equivalent. The import and attribute syntaxes provide\na more natural Python experience.\n\n### Reentrant Callbacks\n\nPython→Erlang→Python callbacks are fully supported. When Python code calls\nan Erlang function that in turn calls back into Python, the system handles\nthis transparently without deadlocking:\n\n```erlang\n%% Register an Erlang function that calls Python\npy:register_function(double_via_python, fun([X]) -\u003e\n    {ok, Result} = py:call('__main__', double, [X]),\n    Result\nend).\n\n%% Define Python functions\npy:exec(\u003c\u003c\"\ndef double(x):\n    return x * 2\n\ndef process(x):\n    from erlang import call\n    # This calls Erlang, which calls Python's double()\n    doubled = call('double_via_python', x)\n    return doubled + 1\n\"\u003e\u003e).\n\n%% Test the full round-trip\n{ok, 21} = py:call('__main__', process, [10]).\n%% 10 → double_via_python → double(10)=20 → +1 = 21\n```\n\nThe implementation uses a suspension/resume mechanism that frees the dirty\nscheduler while the Erlang callback executes, preventing deadlocks even with\nmultiple levels of nesting.\n\n## Shared State Between Workers\n\nPython workers don't share namespace state, but you can share data via the\nbuilt-in state API:\n\n### From Python\n\n```python\nfrom erlang import state_set, state_get, state_delete, state_keys\nfrom erlang import state_incr, state_decr\n\n# Store data (survives across calls, shared between workers)\nstate_set('my_key', {'data': [1, 2, 3], 'count': 42})\n\n# Retrieve data\nvalue = state_get('my_key')  # {'data': [1, 2, 3], 'count': 42}\n\n# Atomic counters (thread-safe, great for metrics)\nstate_incr('requests')       # returns 1\nstate_incr('requests', 10)   # returns 11\nstate_decr('requests')       # returns 10\n\n# List keys\nkeys = state_keys()  # ['my_key', 'requests', ...]\n\n# Delete\nstate_delete('my_key')\n```\n\n### From Erlang/Elixir\n\n```erlang\n%% Store and fetch\npy:state_store(\u003c\u003c\"my_key\"\u003e\u003e, #{value =\u003e 42}).\n{ok, #{value := 42}} = py:state_fetch(\u003c\u003c\"my_key\"\u003e\u003e).\n\n%% Atomic counters\n1 = py:state_incr(\u003c\u003c\"hits\"\u003e\u003e).\n11 = py:state_incr(\u003c\u003c\"hits\"\u003e\u003e, 10).\n10 = py:state_decr(\u003c\u003c\"hits\"\u003e\u003e).\n\n%% List keys and clear\nKeys = py:state_keys().\npy:state_clear().\n```\n\nThis is backed by ETS with `{write_concurrency, true}`, so counters are atomic and fast.\n\n## Process-Bound Python Environments\n\nEach Erlang process gets its own isolated Python namespace. Variables, imports, and objects defined in one process are invisible to others, even when using the same interpreter.\n\n```erlang\n%% Process A defines state\nspawn(fun() -\u003e\n    Ctx = py:context(1),\n    ok = py:exec(Ctx, \u003c\u003c\"counter = 0\"\u003e\u003e),\n    {ok, 0} = py:eval(Ctx, \u003c\u003c\"counter\"\u003e\u003e)\nend).\n\n%% Process B - same context, but isolated namespace\nspawn(fun() -\u003e\n    Ctx = py:context(1),\n    %% 'counter' is undefined here - different process\n    {error, _} = py:eval(Ctx, \u003c\u003c\"counter\"\u003e\u003e)\nend).\n```\n\nThis enables OTP-style patterns for Python:\n\n```erlang\n-module(py_counter).\n-behaviour(gen_server).\n\ninit([]) -\u003e\n    Ctx = py:context(),\n    ok = py:exec(Ctx, \u003c\u003c\"\nclass Counter:\n    def __init__(self): self.value = 0\n    def incr(self): self.value += 1; return self.value\n\ncounter = Counter()\n\"\u003e\u003e),\n    {ok, #{ctx =\u003e Ctx}}.\n\nhandle_call(incr, _From, #{ctx := Ctx} = State) -\u003e\n    {ok, Value} = py:eval(Ctx, \u003c\u003c\"counter.incr()\"\u003e\u003e),\n    {reply, Value, State}.\n```\n\nResetting Python state is simple: terminate the process. Supervisors can restart it with a fresh environment. No need for manual cleanup.\n\nSee [Process-Bound Environments](docs/process-bound-envs.md) for patterns like ML pipelines, stateful actors, and supervision strategies.\n\n## Async/Await Support\n\nCall Python async functions without blocking:\n\n```erlang\n%% Call an async function\nRef = py:async_call(aiohttp, get, [\u003c\u003c\"https://api.example.com/data\"\u003e\u003e]),\n{ok, Response} = py:async_await(Ref).\n\n%% Gather multiple async calls concurrently\n{ok, Results} = py:async_gather([\n    {aiohttp, get, [\u003c\u003c\"https://api.example.com/users\"\u003e\u003e]},\n    {aiohttp, get, [\u003c\u003c\"https://api.example.com/posts\"\u003e\u003e]},\n    {aiohttp, get, [\u003c\u003c\"https://api.example.com/comments\"\u003e\u003e]}\n]).\n\n%% Stream from async generators\n{ok, Chunks} = py:async_stream(mymodule, async_generator, [args]).\n```\n\n## Parallel Execution with Sub-interpreters\n\nTrue parallelism without GIL contention using Python 3.14+ OWN_GIL sub-interpreters:\n\n```erlang\n%% Execute multiple calls in parallel across OWN_GIL sub-interpreters\n%% Requires Python 3.14+\n{ok, Results} = py:parallel([\n    {math, factorial, [100]},\n    {math, factorial, [200]},\n    {math, factorial, [300]},\n    {math, factorial, [400]}\n]).\n%% Each call runs in its own interpreter with its own GIL\n```\n\nFor Python 3.12/3.13, use SHARED_GIL sub-interpreters (`mode =\u003e subinterp`) for namespace isolation, but note that parallelism is limited by the shared GIL.\n\n## Parallel Processing with BEAM Processes\n\nLeverage Erlang's lightweight processes for massive parallelism:\n\n```erlang\n%% Register parallel map function\npy:register_function(parallel_map, fun([FuncName, Items]) -\u003e\n    Parent = self(),\n    Refs = [begin\n        Ref = make_ref(),\n        spawn(fun() -\u003e\n            Result = execute(FuncName, Item),\n            Parent ! {Ref, Result}\n        end),\n        Ref\n    end || Item \u003c- Items],\n    [receive {Ref, R} -\u003e R after 5000 -\u003e timeout end || Ref \u003c- Refs]\nend).\n\n%% Call from Python - processes 10 items in parallel\n{ok, Results} = py:eval(\n    \u003c\u003c\"__import__('erlang').call('parallel_map', 'compute', items)\"\u003e\u003e,\n    #{items =\u003e lists:seq(1, 10)}\n).\n```\n\n**Benchmark Results** (from `examples/erlang_concurrency.erl`):\n```\nSequential: 10 Python calls × 100ms each = 1.01 seconds\nParallel:   10 BEAM processes calling Python = 0.10 seconds\n```\n\nThe speedup is linear with the number of items when work is I/O-bound or\ndistributed across sub-interpreters.\n\n## Virtual Environment Support\n\n```erlang\n%% Activate a venv\nok = py:activate_venv(\u003c\u003c\"/path/to/venv\"\u003e\u003e).\n\n%% Use packages from venv\n{ok, Model} = py:call(sentence_transformers, 'SentenceTransformer', [\u003c\u003c\"all-MiniLM-L6-v2\"\u003e\u003e]).\n\n%% Deactivate when done\nok = py:deactivate_venv().\n```\n\n## Logging and Tracing\n\n### Python Logging to Erlang Logger\n\nForward Python `logging` messages to Erlang's `logger`:\n\n```erlang\n%% Configure Python logging\nok = py:configure_logging(#{level =\u003e info}).\n\n%% Python logs now appear in Erlang logger\nok = py:exec(\u003c\u003c\"\nimport logging\nlogging.info('Hello from Python!')\nlogging.warning('Something needs attention')\n\"\u003e\u003e).\n```\n\nFrom Python, you can also set up logging explicitly:\n\n```python\nimport erlang\nerlang.setup_logging(level=20)  # 20 = INFO\n```\n\n### Distributed Tracing\n\nCollect trace spans from Python code:\n\n```erlang\n%% Enable tracing\nok = py:enable_tracing().\n\n%% Run Python code with spans\nok = py:exec(\u003c\u003c\"\nimport erlang\n\nwith erlang.Span('process-request', user_id=123):\n    with erlang.Span('query-database'):\n        pass  # database work\n    with erlang.Span('format-response'):\n        pass  # formatting work\n\"\u003e\u003e).\n\n%% Retrieve collected spans\n{ok, Spans} = py:get_traces().\n%% Spans = [#{name =\u003e \u003c\u003c\"query-database\"\u003e\u003e, status =\u003e ok, duration_us =\u003e 42, ...}, ...]\n\n%% Clean up\nok = py:clear_traces().\nok = py:disable_tracing().\n```\n\nUse the `@erlang.trace()` decorator for automatic function tracing:\n\n```python\nimport erlang\n\n@erlang.trace()\ndef my_function():\n    return compute_something()\n```\n\nSee [docs/logging.md](docs/logging.md) for details.\n\n## Examples\n\nThe `examples/` directory contains runnable demonstrations:\n\n### Semantic Search\n```bash\n# Setup\npython3 -m venv /tmp/ai-venv\n/tmp/ai-venv/bin/pip install sentence-transformers numpy\n\n# Run\nescript examples/semantic_search.erl\n```\n\n### RAG (Retrieval-Augmented Generation)\n```bash\n# Setup (also install Ollama and pull a model)\n/tmp/ai-venv/bin/pip install sentence-transformers numpy requests\nollama pull llama3.2\n\n# Run\nescript examples/rag_example.erl\n```\n\n### AI Chat\n```bash\nescript examples/ai_chat.erl\n```\n\n### Erlang Concurrency from Python\n```bash\n# Demonstrates 10x speedup with BEAM processes\nescript examples/erlang_concurrency.erl\n```\n\n### Elixir Integration\n```bash\nelixir --erl \"-pa _build/default/lib/erlang_python/ebin\" examples/elixir_example.exs\n```\n\n### Logging and Tracing\n```bash\nescript examples/logging_example.erl\n```\n\n## API Reference\n\n### Function Calls\n\n```erlang\n{ok, Result} = py:call(Module, Function, Args).\n{ok, Result} = py:call(Module, Function, Args, KwArgs).\n{ok, Result} = py:call(Module, Function, Args, KwArgs, Timeout).\n\n%% Async with result\nRef = py:spawn_call(Module, Function, Args).\n{ok, Result} = py:await(Ref).\n{ok, Result} = py:await(Ref, Timeout).\n\n%% Fire-and-forget (no result returned)\nok = py:cast(Module, Function, Args).\n```\n\n### Expression Evaluation\n\n```erlang\n{ok, 42} = py:eval(\u003c\u003c\"21 * 2\"\u003e\u003e).\n{ok, 100} = py:eval(\u003c\u003c\"x * y\"\u003e\u003e, #{x =\u003e 10, y =\u003e 10}).\n{ok, Result} = py:eval(Expression, Locals, Timeout).\n```\n\n### Streaming\n\n```erlang\n{ok, Chunks} = py:stream(Module, GeneratorFunc, Args).\n{ok, [0,1,4,9,16]} = py:stream_eval(\u003c\u003c\"(x**2 for x in range(5))\"\u003e\u003e).\n```\n\n### Callbacks\n\n```erlang\npy:register_function(Name, fun([Args]) -\u003e Result end).\npy:register_function(Name, Module, Function).\npy:unregister_function(Name).\n```\n\n### Memory and GC\n\n```erlang\n{ok, Stats} = py:memory_stats().\n{ok, Collected} = py:gc().\nok = py:tracemalloc_start().\nok = py:tracemalloc_stop().\n```\n\n### Logging\n\n```erlang\nok = py:configure_logging().\nok = py:configure_logging(#{level =\u003e info, format =\u003e \u003c\u003c\"%(name)s: %(message)s\"\u003e\u003e}).\n```\n\n### Tracing\n\n```erlang\nok = py:enable_tracing().\nok = py:disable_tracing().\n{ok, Spans} = py:get_traces().\nok = py:clear_traces().\n```\n\n## Type Mappings\n\n### Erlang to Python\n\n| Erlang | Python |\n|--------|--------|\n| `integer()` | `int` |\n| `float()` | `float` |\n| `binary()` | `str` |\n| `atom()` | `str` |\n| `true` / `false` | `True` / `False` |\n| `none` / `nil` | `None` |\n| `list()` | `list` |\n| `tuple()` | `tuple` |\n| `map()` | `dict` |\n\n### Python to Erlang\n\n| Python | Erlang |\n|--------|--------|\n| `int` | `integer()` |\n| `float` | `float()` |\n| `str` | `binary()` |\n| `bytes` | `binary()` |\n| `True` / `False` | `true` / `false` |\n| `None` | `none` |\n| `list` | `list()` |\n| `tuple` | `tuple()` |\n| `dict` | `map()` |\n\n## Configuration\n\n```erlang\n%% sys.config\n[\n  {erlang_python, [\n    {num_workers, 4},           %% Python worker pool size\n    {max_concurrent, 17},       %% Max concurrent operations (default: schedulers * 2 + 1)\n    {num_executors, 4}          %% Executor threads (multi-executor mode)\n  ]}\n].\n```\n\n## Execution Modes\n\n### Context Modes\n\nWhen creating Python contexts, you can choose the execution mode:\n\n| Mode | Python Version | Description |\n|------|----------------|-------------|\n| `worker` | Any | Main interpreter, shared namespace (default, recommended) |\n| `subinterp` | 3.12+ | SHARED_GIL sub-interpreter, isolated namespace |\n| `owngil` | 3.14+ | OWN_GIL sub-interpreter, true parallelism |\n\n```erlang\n%% Default: worker mode (recommended)\n%% With free-threaded Python (3.13t+), provides true parallelism automatically\n{ok, Ctx} = py_context:new(#{}).\n\n%% Explicit subinterpreter with shared GIL (Python 3.12+)\n%% Provides namespace isolation but no parallelism\n{ok, Ctx} = py_context:new(#{mode =\u003e subinterp}).\n\n%% OWN_GIL mode for true parallelism (Python 3.14+ required)\n%% Each context runs in its own pthread with independent GIL\n{ok, Ctx} = py_context:new(#{mode =\u003e owngil}).\n```\n\n**Worker mode is recommended** because it works with any Python version and automatically benefits from free-threaded Python (3.13t+) when available.\n\n**Why OWN_GIL requires Python 3.14+**: Some C extensions (e.g., `_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 with caveats for C extensions with global state.\n\n### Runtime Detection\n\nCheck the current execution mode:\n```erlang\npy:execution_mode().  %% =\u003e free_threaded | subinterp | multi_executor\n```\n\n| Mode | Python Version | Parallelism |\n|------|----------------|-------------|\n| Free-threaded | 3.13+ (nogil) | True parallel, no GIL |\n| Sub-interpreter | 3.12+ | Per-interpreter GIL |\n| Multi-executor | Any | GIL contention |\n\n## Error Handling\n\n```erlang\n{error, {'NameError', \"name 'x' is not defined\"}} = py:eval(\u003c\u003c\"x\"\u003e\u003e).\n{error, {'ZeroDivisionError', \"division by zero\"}} = py:eval(\u003c\u003c\"1/0\"\u003e\u003e).\n{error, timeout} = py:eval(\u003c\u003c\"sum(range(10**9))\"\u003e\u003e, #{}, 100).\n```\n\n## Documentation\n\n- [Getting Started](docs/getting-started.md)\n- [Process-Bound Environments](docs/process-bound-envs.md) - Isolated Python state per Erlang process\n- [AI Integration Guide](docs/ai-integration.md)\n- [Type Conversion](docs/type-conversion.md)\n- [Context Affinity](docs/context-affinity.md)\n- [Scalability](docs/scalability.md)\n- [Streaming](docs/streaming.md)\n- [Threading](docs/threading.md)\n- [Logging and Tracing](docs/logging.md)\n- [Asyncio Event Loop](docs/asyncio.md) - Erlang-native asyncio with TCP/UDP support\n- [Reactor](docs/reactor.md) - FD-based protocol handling\n- [Security](docs/security.md) - Sandbox and blocked operations\n- [Changelog](https://github.com/benoitc/erlang-python/releases)\n\n## License\n\nApache-2.0\n","funding_links":["https://github.com/sponsors/benoitc"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbenoitc%2Ferlang-python","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbenoitc%2Ferlang-python","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbenoitc%2Ferlang-python/lists"}