Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions c_src/py_convert.c
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,27 @@ static PyObject *term_to_py(ErlNifEnv *env, ERL_NIF_TERM term) {
return PyBytes_FromStringAndSize((char *)bin.data, bin.size);
}

/*
* Check for {bytes, Binary} tagged tuple - explicit bytes conversion.
* This allows users to explicitly send raw bytes without UTF-8 decoding.
*/
{
int tuple_arity;
const ERL_NIF_TERM *tuple_elements;
if (enif_get_tuple(env, term, &tuple_arity, &tuple_elements) && tuple_arity == 2) {
char tag_buf[16];
if (enif_get_atom(env, tuple_elements[0], tag_buf, sizeof(tag_buf), ERL_NIF_LATIN1)) {
if (strcmp(tag_buf, "bytes") == 0) {
ErlNifBinary bytes_bin;
if (enif_inspect_binary(env, tuple_elements[1], &bytes_bin)) {
return PyBytes_FromStringAndSize((char *)bytes_bin.data, bytes_bin.size);
}
/* Not a binary - fall through to normal tuple handling */
}
}
}
}

/* Check list (must come after binary to preserve structure) */
if (enif_get_list_length(env, term, &list_len)) {
PyObject *list = PyList_New(list_len);
Expand Down
33 changes: 33 additions & 0 deletions docs/type-conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ When calling Python functions or evaluating expressions, Erlang values are autom
| `integer()` | `int` | Arbitrary precision supported |
| `float()` | `float` | IEEE 754 double precision |
| `binary()` | `str` | UTF-8 encoded |
| `{bytes, binary()}` | `bytes` | Explicit bytes (no UTF-8 decode) |
| `atom()` | `str` | Converted to string (except special atoms) |
| `true` | `True` | Boolean |
| `false` | `False` | Boolean |
Expand Down Expand Up @@ -56,6 +57,38 @@ py:call(mymod, func, [{1, 2, 3}]). %% Python receives: (1, 2, 3)
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:

```erlang
%% 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()`:

```erlang
%% 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:
Expand Down
40 changes: 38 additions & 2 deletions test/py_api_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
test_context_management/1,
test_start_stop_contexts/1,
%% Mixed usage
test_mixed_api_usage/1
test_mixed_api_usage/1,
%% Type conversion
test_explicit_bytes/1
]).

%% ============================================================================
Expand All @@ -52,7 +54,9 @@ all() ->
test_context_management,
test_start_stop_contexts,
%% Mixed usage
test_mixed_api_usage
test_mixed_api_usage,
%% Type conversion
test_explicit_bytes
].

init_per_suite(Config) ->
Expand Down Expand Up @@ -223,3 +227,35 @@ test_mixed_api_usage(_Config) ->
%% Both should work correctly
{ok, 6} = py:eval(<<"2 + 4">>),
{ok, 7} = py:eval(Ctx, <<"3 + 4">>, #{}).

%% ============================================================================
%% Type Conversion Tests
%% ============================================================================

%% @doc Test explicit bytes conversion using {bytes, Binary} tuple.
test_explicit_bytes(_Config) ->
Ctx = py:context(1),

%% Define test functions
ok = py:exec(Ctx, <<"
def check_type(val):
return type(val).__name__

def check_bytes_value(val):
return val == b'hello'
">>),

%% Regular binary -> str (default UTF-8 decoding)
{ok, <<"str">>} = py:eval(Ctx, <<"check_type(val)">>, #{<<"val">> => <<"hello">>}),

%% Explicit bytes tuple -> bytes
{ok, <<"bytes">>} = py:eval(Ctx, <<"check_type(val)">>, #{<<"val">> => {bytes, <<"hello">>}}),

%% Verify value is correct
{ok, true} = py:eval(Ctx, <<"check_bytes_value(val)">>, #{<<"val">> => {bytes, <<"hello">>}}),

%% Test with binary data (non-UTF8)
NonUtf8 = <<255, 254, 0, 1>>,
{ok, <<"bytes">>} = py:eval(Ctx, <<"check_type(val)">>, #{<<"val">> => {bytes, NonUtf8}}),

ok.
Loading