Type Conversion

View Source

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:

ErlangPythonNotes
integer()intArbitrary precision supported
float()floatIEEE 754 double precision
binary()strUTF-8 encoded
{bytes, binary()}bytesExplicit bytes (no UTF-8 decode)
atom()strConverted to string (except special atoms)
trueTrueBoolean
falseFalseBoolean
noneNoneNull value
nilNoneNull value (Elixir compatibility)
undefinedNoneNull value
list()listRecursively converted
tuple()tupleRecursively converted
map()dictKeys and values recursively converted
pid()erlang.PidOpaque 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}

Explicit Bytes Conversion

By default, Erlang binaries are converted to Python str using UTF-8 decoding. To explicitly send raw bytes without string conversion, use the {bytes, Binary} tuple:

%% Default: binary -> str
py:call(mymod, func, [<<"hello">>]).  %% Python sees: "hello" (str)

%% Explicit: {bytes, binary} -> bytes
py:call(mymod, func, [{bytes, <<"hello">>}]).  %% Python sees: b"hello" (bytes)

%% Useful for binary protocols, images, compressed data
py:call(image_processor, load, [{bytes, ImageData}]).

This is useful when you need to ensure binary data is treated as raw bytes in Python, for example when working with binary protocols, image data, or compressed content.

Note that on the return path, both Python str and bytes become Erlang binary():

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

%% Python bytes -> Erlang binary
{ok, <<"hello">>} = py:eval(<<"b'hello'">>).

%% Non-UTF8 bytes also work
{ok, <<255, 254>>} = py:eval(<<"b'\\xff\\xfe'">>).

Python to Erlang

Return values from Python are converted back to Erlang:

PythonErlangNotes
intinteger() or string()Integers > 64 bits returned as strings
floatfloat()IEEE 754 double precision
float('nan')nanAtom for Not-a-Number
float('inf')infinityAtom for positive infinity
float('-inf')neg_infinityAtom for negative infinity
strbinary()UTF-8 encoded
bytesbinary()Raw bytes
TruetrueBoolean
FalsefalseBoolean
NonenoneNull value
listlist()Recursively converted
tupletuple()Recursively converted
dictmap()Keys and values recursively converted
erlang.Pidpid()Round-trips back to the original Erlang PID
generatorinternalUsed with streaming functions

Examples

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

%% Big integers (> 64 bits) are returned as strings
{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 TypeWorkaround
setConvert to list: list(my_set)
frozensetConvert to tuple: tuple(my_frozenset)
datetimeUse .isoformat() or timestamp
DecimalUse float() or str()
Custom objectsImplement __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: Integers larger than 64 bits are returned as strings; convert with list_to_integer/1 if needed
  • 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