Skip to content

Latest commit

 

History

History
257 lines (189 loc) · 7.33 KB

File metadata and controls

257 lines (189 loc) · 7.33 KB

Type Conversion

This guide details how values are converted between Erlang and Python.

Erlang to Python

When calling Python functions or evaluating expressions, Erlang values are automatically converted:

Erlang Python Notes
integer() int Arbitrary precision supported
float() float IEEE 754 double precision
binary() str UTF-8 encoded
atom() str Converted to string (except special atoms)
true True Boolean
false False Boolean
none None Null value
nil None Null value (Elixir compatibility)
undefined None Null value
list() list Recursively converted
tuple() tuple Recursively converted
map() dict Keys and values recursively converted
pid() erlang.Pid Opaque wrapper, round-trips back to Erlang PID

Examples

%% Integers
py:call(mymod, func, [42]).           %% Python receives: 42
py:call(mymod, func, [123456789012345678901234567890]).  %% Big integers work

%% Floats
py:call(mymod, func, [3.14159]).      %% Python receives: 3.14159

%% Strings (binaries)
py:call(mymod, func, [<<"hello">>]).  %% Python receives: "hello"

%% Atoms become strings
py:call(mymod, func, [foo]).          %% Python receives: "foo"

%% Booleans
py:call(mymod, func, [true, false]).  %% Python receives: True, False

%% None equivalents
py:call(mymod, func, [none]).         %% Python receives: None
py:call(mymod, func, [nil]).          %% Python receives: None
py:call(mymod, func, [undefined]).    %% Python receives: None

%% Lists
py:call(mymod, func, [[1, 2, 3]]).    %% Python receives: [1, 2, 3]

%% Tuples
py:call(mymod, func, [{1, 2, 3}]).    %% Python receives: (1, 2, 3)

%% Maps become dicts
py:call(mymod, func, [#{a => 1, b => 2}]).  %% Python receives: {"a": 1, "b": 2}

Python to Erlang

Return values from Python are converted back to Erlang:

Python Erlang Notes
int integer() Arbitrary precision supported
float float() IEEE 754 double precision
float('nan') nan Atom for Not-a-Number
float('inf') infinity Atom for positive infinity
float('-inf') neg_infinity Atom for negative infinity
str binary() UTF-8 encoded
bytes binary() Raw bytes
True true Boolean
False false Boolean
None none Null value
list list() Recursively converted
tuple tuple() Recursively converted
dict map() Keys and values recursively converted
erlang.Pid pid() Round-trips back to the original Erlang PID
generator internal Used with streaming functions

Examples

%% Integers
{ok, 42} = py:eval(<<"42">>).
{ok, 123456789012345678901234567890} = py:eval(<<"123456789012345678901234567890">>).

%% Floats
{ok, 3.14} = py:eval(<<"3.14">>).

%% Special floats
{ok, nan} = py:eval(<<"float('nan')">>).
{ok, infinity} = py:eval(<<"float('inf')">>).
{ok, neg_infinity} = py:eval(<<"float('-inf')">>).

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

%% Bytes
{ok, <<72,101,108,108,111>>} = py:eval(<<"b'Hello'">>).

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

%% None
{ok, none} = py:eval(<<"None">>).

%% Lists
{ok, [1, 2, 3]} = py:eval(<<"[1, 2, 3]">>).

%% Tuples
{ok, {1, 2, 3}} = py:eval(<<"(1, 2, 3)">>).

%% Dicts become maps
{ok, #{<<"a">> := 1, <<"b">> := 2}} = py:eval(<<"{'a': 1, 'b': 2}">>).

Process Identifiers (PIDs)

Erlang PIDs are converted to opaque erlang.Pid objects in Python. These can be passed back to Erlang (where they become real PIDs again) or used with erlang.send():

%% Pass self() to Python - arrives as erlang.Pid
{ok, Pid} = py:call(mymod, round_trip_pid, [self()]).
%% Pid =:= self()

%% Python can send messages directly to Erlang processes
ok = py:exec(<<"
import erlang
def notify(pid, data):
    erlang.send(pid, ('notification', data))
">>).
import erlang

def forward_to(pid, message):
    """Send a message to an Erlang process."""
    erlang.send(pid, message)

erlang.Pid objects support equality and hashing, so they can be compared and used as dict keys or in sets:

pid_a == pid_b       # True if both wrap the same Erlang PID
{pid: "value"}       # Works as a dict key
pid in seen_pids     # Works in sets

Sending to a process that has already exited raises erlang.ProcessError.

Special Cases

NumPy Arrays

NumPy arrays are converted to nested Erlang lists:

%% 1D array
{ok, [1.0, 2.0, 3.0]} = py:eval(<<"import numpy as np; np.array([1, 2, 3]).tolist()">>).

%% 2D array
{ok, [[1, 2], [3, 4]]} = py:eval(<<"import numpy as np; np.array([[1,2],[3,4]]).tolist()">>).

For best performance with large arrays, consider using .tolist() in Python before returning.

Nested Structures

Nested data structures are recursively converted:

%% Nested dict
{ok, #{<<"user">> := #{<<"name">> := <<"Alice">>, <<"age">> := 30}}} =
    py:eval(<<"{'user': {'name': 'Alice', 'age': 30}}">>).

%% List of tuples
{ok, [{1, <<"a">>}, {2, <<"b">>}]} = py:eval(<<"[(1, 'a'), (2, 'b')]">>).

%% Mixed nesting
{ok, #{<<"items">> := [1, 2, 3], <<"meta">> := {<<"ok">>, 200}}} =
    py:eval(<<"{'items': [1, 2, 3], 'meta': ('ok', 200)}">>).

Map Keys

Erlang maps support any term as key, but Python dicts are more restricted:

%% Erlang atom keys become Python strings
py:call(json, dumps, [#{foo => 1, bar => 2}]).
%% Python sees: {"foo": 1, "bar": 2}

%% Binary keys stay as strings
py:call(json, dumps, [#{<<"foo">> => 1}]).
%% Python sees: {"foo": 1}

When Python returns dicts, string keys become binaries:

{ok, #{<<"foo">> := 1}} = py:eval(<<"{'foo': 1}">>).

Keyword Arguments

Maps can be used for Python keyword arguments:

%% Call with kwargs
{ok, Json} = py:call(json, dumps, [Data], #{indent => 2, sort_keys => true}).

%% Equivalent Python: json.dumps(data, indent=2, sort_keys=True)

Unsupported Types

Some Python types cannot be directly converted:

Python Type Workaround
set Convert to list: list(my_set)
frozenset Convert to tuple: tuple(my_frozenset)
datetime Use .isoformat() or timestamp
Decimal Use float() or str()
Custom objects Implement __iter__ or serialization

Example Workarounds

%% Sets - convert to list in Python
{ok, [1, 2, 3]} = py:eval(<<"sorted(list({3, 1, 2}))">>).

%% Datetime - use ISO format
{ok, <<"2024-01-15T10:30:00">>} =
    py:eval(<<"from datetime import datetime; datetime(2024,1,15,10,30).isoformat()">>).

%% Decimal - convert to string for precision
{ok, <<"3.14159265358979323846">>} =
    py:eval(<<"from decimal import Decimal; str(Decimal('3.14159265358979323846'))">>).

Performance Considerations

  • Large strings: Binary conversion is efficient, but very large strings may cause memory pressure
  • Deep nesting: Deeply nested structures require recursive traversal
  • Big integers: Arbitrary precision integers work but large ones are slower
  • NumPy arrays: Call .tolist() for explicit conversion; direct array conversion may be slower

For large data transfers, consider:

  1. Using streaming for iterables
  2. Serializing to JSON/msgpack in Python
  3. Processing data in chunks