Releases: benoitc/erlang-python
v1.6.0
Added
-
Python Logging Integration - Forward Python's
loggingmodule to Erlang'sloggerpy:configure_logging/0,1- Setup Python logging to forward to Erlangerlang.ErlangHandler- Python logging handler that sends to Erlangerlang.setup_logging(level, format)- Configure logging from Python- Fire-and-forget architecture using
enif_send()for non-blocking messaging - Level filtering at NIF level for performance
- Thread-safe - works from any Python thread
-
Distributed Tracing - Collect trace spans from Python code
py:enable_tracing/0,py:disable_tracing/0- Enable/disable span collectionpy:get_traces/0- Retrieve collected spanserlang.Span(name, **attrs)- Context manager for creating spanserlang.trace(name)- Decorator for tracing functions- Automatic parent/child span linking via thread-local storage
-
New Erlang modules:
py_logger,py_tracer
Performance
-
Type conversion optimizations - Faster Python ↔ Erlang marshalling
- Use
enif_is_identicalfor atom comparison instead ofstrcmp - Stack allocate small tuples/maps (≤16 elements) to avoid heap allocation
- Use
enif_make_map_from_arraysfor O(n) map building vs O(n²) puts
- Use
-
Fire-and-forget NIF architecture - Log and trace calls never block Python execution
Fixed
-
Python 3.12+ event loop thread isolation - Fixed asyncio timeouts on Python 3.12+
ErlangEventLoopnow only used for main thread; worker threads getSelectorEventLoop- Per-call
ErlNifEnvfor thread-safe timer scheduling in free-threaded mode - Fail-fast error handling in
erlang_loop.pyinstead of silent hangs - Added
gil_acquire()/gil_release()helpers to avoid GIL double-acquisition
-
Intermittent test failures on free-threaded Python - Added startup synchronization
v1.5.0
Added
-
py_asgimodule - Optimized ASGI request handling with:- Pre-interned Python string keys (15+ ASGI scope keys)
- Cached constant values (http type, HTTP versions, methods, schemes)
- Thread-local response pooling (16 slots per thread, 4KB initial buffer)
- Direct NIF path bypassing generic py:call()
- ~60-80% throughput improvement over py:call()
- Configurable runner module via
runneroption - Sub-interpreter and free-threading (Python 3.13+) support
-
py_wsgimodule - Optimized WSGI request handling with:- Pre-interned WSGI environ keys
- Direct NIF path for marshalling
- ~60-80% throughput improvement over py:call()
- Sub-interpreter and free-threading support
-
Web frameworks documentation - New documentation at
docs/web-frameworks.md
v1.4.0 - Erlang-native asyncio event loop
Added
-
Erlang-native asyncio event loop - Custom asyncio event loop backed by Erlang's scheduler
ErlangEventLoopclass inpriv/erlang_loop.py- Sub-millisecond latency via Erlang's
enif_select(vs 10ms polling) - Zero CPU usage when idle - no busy-waiting or polling overhead
- Full GIL release during waits for better concurrency
- Native Erlang scheduler integration for I/O events
- Event loop policy via
get_event_loop_policy()
-
TCP support for asyncio event loop
create_connection()- TCP client connectionscreate_server()- TCP server with accept loop_ErlangSocketTransport- Non-blocking socket transport with write buffering_ErlangServer- TCP server withserve_forever()support
-
UDP/datagram support for asyncio event loop
create_datagram_endpoint()- Create UDP endpoints with full parameter support_ErlangDatagramTransport- Datagram transport implementation- Parameters:
local_addr,remote_addr,reuse_address,reuse_port,allow_broadcast DatagramProtocolcallbacks:datagram_received(),error_received()- Support for both connected and unconnected UDP
-
Asyncio event loop documentation
- New documentation:
docs/asyncio.md
- New documentation:
Performance
- Event loop optimizations
- Fixed
run_until_completecallback removal bug - Cached
ast.literal_evallookup at module initialization - O(1) timer cancellation via handle-to-callback_id reverse map
- Detach pending queue under mutex, build Erlang terms outside lock
- O(1) duplicate event detection using hash set
- Added
PERF_BUILDcmake option for aggressive optimizations (-O3, LTO, -march=native)
- Fixed
Full Changelog: https://github.com/benoitc/erlang-python/blob/main/CHANGELOG.md
1.3.2
Fixed
- torch/PyTorch introspection compatibility - Fixed
AttributeError: 'erlang.Function' object has no attribute 'endswith'when importing torch or sentence_transformers in contexts where erlang_python callbacks are registered.- Root cause: torch does dynamic introspection during import, iterating through Python's namespace and calling
.endswith()on objects. Theerlangmodule's__getattr__was returningErlangFunctionwrappers for any attribute access. - Solution: Added C-side callback name registry. Now
__getattr__only returnsErlangFunctionwrappers for actually registered callbacks. Unregistered attributes raiseAttributeError(normal Python behavior). - New test:
test_callback_name_registryinpy_reentrant_SUITE.erl
- Root cause: torch does dynamic introspection during import, iterating through Python's namespace and calling
1.3.1
v1.3.0
Added
- Asyncio Support - New
erlang.async_call()for asyncio-compatible callbacksawait erlang.async_call('func', arg1, arg2)- Call Erlang from async Python code- Integrates with asyncio event loop via
add_reader() - No exceptions raised for control flow (unlike
erlang.call()) - Releases dirty NIF thread while waiting (non-blocking)
- Works with FastAPI, Starlette, aiohttp, and other ASGI frameworks
- Supports concurrent calls via
asyncio.gather()
Fixed
- Flag-based callback detection in replay path - Fixed SuspensionRequired exceptions leaking when ASGI middleware catches and re-raises exceptions
Changed
- C code optimizations and refactoring
- Thread safety fixes: Used
pthread_oncefor async callback initialization - Timeout handling: Added
read_with_timeout()andread_length_prefixed_data()helpers - Code deduplication: Merged suspended state creation functions, extracted helpers
- Performance: Optimized list conversion using
enif_make_list_cell()
- Thread safety fixes: Used
v1.2.0
Added
-
Context Affinity - Bind Erlang processes to dedicated Python workers for state persistence
py:bind()/py:unbind()- Bind current process to a worker, preserving Python statepy:bind(new)- Create explicit context handles for multiple contexts per processpy:with_context(Fun)- Scoped helper with automatic bind/unbind- Context-aware functions:
py:ctx_call/4-6,py:ctx_eval/2-4,py:ctx_exec/2 - Automatic cleanup via process monitors when bound processes die
- O(1) ETS-based binding lookup for minimal overhead
- New test suite:
test/py_context_SUITE.erl
-
Python Thread Support - Any spawned Python thread can now call
erlang.call()without blocking- Supports
threading.Thread,concurrent.futures.ThreadPoolExecutor, and any other Python threads - Each spawned thread lazily acquires a dedicated "thread worker" channel
- One lightweight Erlang process per Python thread handles callbacks
- Automatic cleanup when Python thread exits via
pthread_key_tdestructor - New module:
py_thread_handler.erl- Coordinator and per-thread handlers - New C file:
py_thread_worker.c- Thread worker pool management - New test suite:
test/py_thread_callback_SUITE.erl - New documentation:
docs/threading.md- Threading support guide
- Supports
-
Reentrant Callbacks - Python→Erlang→Python callback chains without deadlocks
- Exception-based suspension mechanism interrupts Python execution cleanly
- Callbacks execute in separate processes to prevent worker pool exhaustion
- Supports arbitrarily deep nesting (tested up to 10+ levels)
- Transparent to users -
erlang.call()works the same, just without deadlocks - New test suite:
test/py_reentrant_SUITE.erl - New examples:
examples/reentrant_demo.erlandexamples/reentrant_demo.py
Changed
- Callback handlers now spawn separate processes for execution, allowing workers
to remain available for nestedpy:eval/py:calloperations - Modular C code structure - Split monolithic
py_nif.c(4,335 lines) into
logical modules for better maintainability:py_nif.h- Shared header with types, macros, and declarationspy_convert.c- Bidirectional type conversion (Python ↔ Erlang)py_exec.c- Python execution engine and GIL managementpy_callback.c- Erlang callback support and asyncio integration- Uses
#includeapproach for single compilation unit (no build changes needed)
Fixed
- Multiple sequential erlang.call() - Fixed infinite loop when Python code makes
multiple sequentialerlang.call()invocations in the same function. The replay
mechanism now falls back to blocking pipe behavior for subsequent calls after the
first suspension, preventing the infinite replay loop. - Memory safety in C NIF - Fixed memory leaks and added NULL checks
nif_async_worker_new: msg_env now freed on pipe/thread creation failuremulti_executor_stop: shutdown requests now properly freed after joincreate_suspended_state: binary allocations cleaned up on failure paths- Added NULL checks on all
enif_alloc_resourceandenif_alloc_envcalls
- Dialyzer warnings - Added
{suspended, ...}return type to NIF specs for
worker_call,worker_eval, andresume_callbackfunctions - Dead code removal - Cleaned up unused code discovered during code review:
- Removed
execute_direct()function inpy_exec.c(duplicated inline logic) - Removed unused
reffield fromasync_pending_tstruct inpy_nif.h - Removed
worker_recv/2frompy_nif.erl(declared but never implemented in C)
- Removed
Documentation
- Doxygen-style C documentation - Added documentation to all C source files:
- Architecture overview with execution mode diagrams
- Type mapping tables for conversions
- GIL management patterns and best practices
- Suspension/resume flow diagrams for callbacks
- Function-level
@param,@return,@pre,@warning,@seeannotations
v1.1.0
Added
-
Shared State API - ETS-backed storage for sharing data between Python workers
state_set/get/delete/keys/clearaccessible from Python viafrom erlang import ...py:state_store/fetch/remove/keys/clearfrom Erlang- Atomic counters with
state_incr/decr(Python) andpy:state_incr/decr(Erlang) - New example:
examples/shared_state_example.erl
-
Native Python Import Syntax for Erlang callbacks
from erlang import my_func; my_func(args)- most Pythonicerlang.my_func(args)- attribute-style accesserlang.call('my_func', args)- legacy syntax still works
-
Module Reload - Reload Python modules across all workers during development
py:reload(module)usesimportlib.reload()to refresh modules from diskpy_pool:broadcastfor sending requests to all workers
-
Documentation improvements
- Added shared state section to getting-started, scalability, and ai-integration guides
- Added embedding caching example using shared state
- Added hex.pm badges to README
Fixed
- Memory safety - Added NULL checks to all
enif_alloc()calls in NIF code - Worker resilience - Fixed crash in
py_subinterp_pool:terminatewhen workers undefined - Streaming example - Fixed to work with worker pool design (workers don't share namespace)
- ETS table ownership - Moved
py_callbackstable creation to supervisor for resilience
Changed
- Created
py_utilmodule to consolidate duplicate code (to_binary/1,send_response/3,normalize_timeout/1-2) - Consolidated
async_await/2to callawait/2reducing duplication
v1.0.0
Initial release of erlang_python - Execute Python from Erlang/Elixir using dirty NIFs.
Features
-
Python Integration
- Call Python functions with
py:call/3-5 - Evaluate expressions with
py:eval/1-3 - Execute statements with
py:exec/1-2 - Stream from Python generators with
py:stream/3-4
- Call Python functions with
-
Multiple Execution Modes (auto-detected)
- Free-threaded Python 3.13+ (no GIL, true parallelism)
- Sub-interpreters Python 3.12+ (per-interpreter GIL)
- Multi-executor for older Python versions
-
Worker Pools
- Main worker pool for synchronous calls
- Async worker pool for asyncio coroutines
- Sub-interpreter pool for parallel execution
-
Erlang/Elixir Callbacks
- Register functions callable from Python via
py:register_function/2-3 - Python code calls back with
erlang.call('name', args...)
- Register functions callable from Python via
-
Virtual Environment Support
- Activate venvs with
py:activate_venv/1 - Use isolated package dependencies
- Activate venvs with
-
Rate Limiting
- ETS-based semaphore prevents overload
- Configurable max concurrent operations
-
Type Conversion
- Automatic conversion between Erlang and Python types
- Integers, floats, strings, lists, tuples, maps/dicts, booleans
-
Memory Management
- Access Python GC stats with
py:memory_stats/0 - Force garbage collection with
py:gc/0-1 - Memory tracing with
py:tracemalloc_start/stop
- Access Python GC stats with
Examples
semantic_search.erl- Text embeddings and similarity searchrag_example.erl- Retrieval-Augmented Generation with Ollamaai_chat.erl- Interactive LLM chaterlang_concurrency.erl- 10x speedup with BEAM processeselixir_example.exs- Full Elixir integration demo
Documentation
- Getting Started guide
- AI Integration guide
- Type Conversion reference
- Scalability and performance tuning
- Streaming with generators