Convert Python code to idiomatic Erlang. Use when migrating Python projects to Erlang, translating Python patterns to idiomatic Erlang, or refactoring Python codebases for fault-tolerance, distributed computing, and concurrency. Extends meta-convert-dev with Python-to-Erlang specific patterns.
Convert Python code to idiomatic Erlang. Use when migrating Python projects to Erlang, translating Python patterns to idiomatic Erlang, or refactoring Python codebases for fault-tolerance, distributed computing, and concurrency. Extends meta-convert-dev with Python-to-Erlang specific patterns.
Convert Python to Erlang
Convert Python code to idiomatic Erlang. This skill extends meta-convert-dev with Python-to-Erlang specific type mappings, idiom translations, and tooling for transforming dynamic, garbage-collected Python code into functional, concurrent, fault-tolerant Erlang.
Think in processes - replace threads/async with lightweight processes
Let it crash - replace defensive exception handling with supervision
Pattern match - use pattern matching instead of if/else chains
Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
Python
Erlang
Notes
int
integer()
Both have arbitrary precision
float
float()
IEEE 754 double precision
bool
true / false
Atoms, not a separate type
str
binary()
UTF-8 binary: <<"hello">>
str
string()
List of codepoints: "hello" = [104,101,108,108,111]
bytes
binary()
Raw binary data
bytearray
binary()
Immutable in Erlang
None
undefined
Atom, or use option pattern
Critical Note on Strings: Erlang has two string representations:
Binaries (<<"text">>): More memory-efficient, preferred for UTF-8 text
Lists ("text"): Lists of integers (codepoints), legacy representation
Use binaries for modern Erlang code.
Collection Types
Python
Erlang
Notes
list[T]
list(T)
Singly-linked, immutable
tuple
tuple()
Fixed-size, immutable, pattern matchable
dict[K, V]
map()
Modern maps (Erlang 17+): #{key => value}
dict[K, V]
dict module
Legacy dict module
set[T]
sets module
sets:new(), sets:add_element()
frozenset[T]
sets module
All Erlang collections are immutable
collections.deque
queue module
queue:new(), FIFO operations
collections.OrderedDict
map()
Maps maintain insertion order (Erlang 18+)
collections.defaultdict
maps:get(Key, Map, Default)
Use default parameter
collections.Counter
map()
Map with integer values
Composite Types
Python
Erlang
Notes
class (data)
record
Compile-time record definition
class (data)
map()
Runtime structured data
class (behavior)
behaviour module
gen_server, gen_statem, etc.
@dataclass
-record(name, {fields})
Record with type specs
typing.Protocol
behaviour
Behavior contracts
typing.TypedDict
map() with type spec
-type my_map() :: #{field := type()}.
typing.NamedTuple
record or tuple
Record preferred for clarity
enum.Enum
atoms
Use atoms for enumerated values
typing.Literal["a", "b"]
atoms
a, b as atoms
typing.Union[T, U]
Tagged tuples
{ok, Value} / {error, Reason}
typing.Optional[T]
`Value
undefined`
typing.Callable[[Args], Ret]
fun()
fun((Args) -> Ret)
Type Annotations → Type Specs
Python
Erlang
Notes
def f(x: int) -> int
-spec f(integer()) -> integer().
Function type specification
def f(x: T) -> T
-spec f(T) -> T when T :: any().
Generic type variable
x: Any
any()
Top type
x: list[int]
[integer()]
List of integers
x: Optional[int]
integer() | undefined
Union type
Idiom Translation
Pattern 1: None Handling (Option Pattern)
Python:
# Optional chaining with walrus operatorif user := get_user(user_id):
name = user.name
else:
name = "Anonymous"# Or simpler
name = user.name if user else"Anonymous"
Erlang:
% Tagged tuple pattern
case get_user(UserId) of
{ok, User} ->
Name = maps:get(name, User);
error ->
Name = <<"Anonymous">>
end.
% Or with pattern matching in function head
get_user_name(UserId) ->
case get_user(UserId) of
{ok, #{name := Name}} -> Name;
error -> <<"Anonymous">>
end.
Erlang's pattern matching is more explicit and compile-time checked
Tagged tuples are the idiomatic Erlang way to represent optional values
Pattern 2: List Comprehensions
Python:
# List comprehension
squared_evens = [x * x for x in numbers if x % 2 == 0]
# Generator expression
total = sum(x * x for x in numbers if x % 2 == 0)
Erlang:
% List comprehension
SquaredEvens = [X * X || X <- Numbers, X rem 2 == 0].
% Fold for aggregation
Total = lists:foldl(
fun(X, Acc) when X rem 2 == 0 -> Acc + X * X;
(_, Acc) -> Acc
end,
0,
Numbers
).
% Or filter + map + sum
Total = lists:sum(
lists:map(
fun(X) -> X * X end,
lists:filter(fun(X) -> X rem 2 == 0 end, Numbers)
)
).
Why this translation:
Erlang list comprehensions are syntactically similar to Python's
Use || instead of for, guards instead of if
For aggregation, lists:foldl/3 is the standard approach
List operations in Erlang are functional transformations
Pattern 3: Dictionary Operations
Python:
# Get with default
value = config.get("timeout", 30)
# Setdefault pattern
cache.setdefault(key, expensive_compute())
# Dictionary comprehension
squared = {k: v * v for k, v in items.items()}
Erlang:
% Get with default
Value = maps:get(timeout, Config, 30).
% Update only if not present (immutable, returns new map)
NewCache = case maps:is_key(Key, Cache) of
true -> Cache;
false -> maps:put(Key, expensive_compute(), Cache)
end.
% Map comprehension (Erlang 18+)
Squared = maps:from_list([{K, V * V} || {K, V} <- maps:to_list(Items)]).
% Or more idiomatically with maps:map/2
Squared = maps:map(fun(_K, V) -> V * V end, Items).
Why this translation:
Erlang maps are immutable; operations return new maps
maps:get/3 takes default as third parameter
maps:map/2 transforms values while preserving keys
No direct equivalent to setdefault because of immutability
Pattern 4: String Formatting
Python:
# f-strings (Python 3.6+)
message = f"User {user.name} has {count} items"# format method
message = "User {} has {} items".format(user.name, count)
# % formatting (old style)
message = "User %s has %d items" % (user.name, count)
Erlang:
% io_lib:format/2 (returns iolist, needs flattening for string)
Message = io_lib:format("User ~s has ~p items", [Name, Count]),
FlatMessage = lists:flatten(Message).
% For binaries (more common in modern Erlang)
Message = iolist_to_binary(io_lib:format("User ~s has ~p items", [Name, Count])).
% String concatenation with binaries
Message = <<<<"User ">>/binary, Name/binary, <<" has ">>/binary,
(integer_to_binary(Count))/binary, <<" items">>/binary>>.
Why this translation:
Erlang uses io_lib:format/2 with format specifiers: ~s (string), ~p (any term), ~w (term), ~.2f (float)
Returns an iolist, not a string - flatten or convert to binary
Binary concatenation is efficient but verbose; use io_lib:format/2 for readability
Pattern 5: Duck Typing → Behaviors
Python:
# Duck typing - if it has a .read() method, it's file-likedefprocess_data(file_like):
data = file_like.read()
return parse(data)
# Works with files, StringIO, BytesIO, etc.
Erlang uses explicit behaviors (-behaviour(gen_server)) instead of duck typing
Behavior modules define callbacks that implementing modules must provide
Process-based abstraction is more common: send messages, receive responses
Compile-time checking of behavior implementations
Pattern 6: Context Managers → Process Lifecycle
Python:
# with statement for resource managementwithopen("data.txt") as f:
data = f.read()
# File automatically closed# Custom context managerwith lock_held(mutex):
# Critical sectionpass# Lock automatically released
% No direct decorator equivalent - use wrapper functions
expensive_func(X) ->
case cache:get(X) of
{ok, Result} -> Result;
error ->
Result = compute(X),
cache:put(X, Result),
Result
end.
% Records don't have methods - use functions
-record(circle, {radius}).
area(#circle{radius = R}) ->
3.14159 * R * R.
% Or use maps with computed access
circle_area(#{radius := R}) ->
3.14159 * R * R.
% Parse transforms for compile-time metaprogramming (advanced)
% Similar to decorators but requires compiler hooks
Why this translation:
Erlang has no decorators; use explicit wrapper functions
Parse transforms can modify AST at compile time (advanced, rarely needed)
Functions are first-class; pass them as arguments for HOF patterns
Records and maps don't have methods; use module functions instead
Error Handling
Python Exceptions → Erlang Error Model
Python
Erlang
When to Use
raise Exception("msg")
error({reason, Msg})
Internal errors, let it crash
raise ValueError("msg")
error(badarg)
Invalid arguments
try...except
try...catch
API boundaries only
try...finally
try...after
Resource cleanup
Exception chaining
Nested tuples
{error, {reason, {cause, SubReason}}}
Exception Translation
Python:
try:
result = risky_operation()
except ValueError as e:
handle_value_error(e)
except KeyError as e:
handle_key_error(e)
except Exception as e:
handle_generic_error(e)
finally:
cleanup()
import threading
defworker(name, delay):
time.sleep(delay)
print(f"Worker {name} done")
threads = [
threading.Thread(target=worker, args=(f"T{i}", 1))
for i inrange(5)
]
for t in threads:
t.start()
for t in threads:
t.join()
Erlang:
worker(Name, Delay) ->
timer:sleep(Delay),
io:format("Worker ~s done~n", [Name]).
main() ->
Pids = [
spawn(fun() -> worker(io_lib:format("T~p", [I]), 1000) end)
|| I <- lists:seq(1, 5)
],
% Wait for all to complete (using monitors)
[begin
Ref = monitor(process, Pid),
receive
{'DOWN', Ref, process, Pid, _} -> ok
end
end || Pid <- Pids].
Treating Erlang as Object-Oriented: Erlang is functional. Don't try to recreate class hierarchies. Use behaviors, modules, and processes.
Excessive Try-Catch: Don't wrap everything in try-catch. Let processes crash and use supervisors for fault recovery.
String Confusion: Erlang strings are lists of integers. Use binaries (<<"text">>) for UTF-8 text in modern code.
Mutable State Mindset: All data is immutable. Operations return new values. Use processes for mutable state via message passing.
List Concatenation in Loops: List ++ Element creates a new list each time (O(n)). Use cons [Element | List] and reverse, or use lists:reverse/2.
Ignoring Process Leaks: Spawned processes live until they exit. Always ensure processes terminate or are linked/monitored.
Not Using Pattern Matching: Erlang's strength is pattern matching. Use it in function heads, case expressions, and receive clauses.
Forgetting Tail Recursion: Recursive functions must be tail-recursive to avoid stack overflow. Use accumulator parameters.
Maps vs Records: Records are compile-time, maps are runtime. Records are faster and type-checked, but less flexible.
Process Bottlenecks: Single process handling all messages becomes a bottleneck. Design for parallelism with process pools or parallel message handling.
Tooling
Tool
Purpose
Notes
rebar3
Build tool
Standard build tool for Erlang projects
dialyzer
Static analyzer
Type checking via success typing
eunit
Unit testing
Built-in unit test framework
common_test
Integration testing
OTP testing framework
PropEr
Property-based testing
Similar to Python's Hypothesis
meck
Mocking library
Mock modules for testing
observer
GUI profiler
Visual process/memory inspector
dbg
Tracing
Built-in tracing for debugging
py2erl
Transpiler
Experimental Python to Erlang compiler
ErlPort
Python interop
Call Python from Erlang
Examples
Example 1: Simple - HTTP Request Handler
Before (Python):
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/user/<int:user_id>', methods=['GET'])defget_user(user_id):
user = database.find_user(user_id)
if user:
return jsonify(user)
else:
return jsonify({"error": "Not found"}), 404if __name__ == '__main__':
app.run()
After (Erlang):
% Using Cowboy web server
-module(user_handler).
-export([init/2]).
init(Req0, State) ->
UserId = cowboy_req:binding(user_id, Req0),
case database:find_user(binary_to_integer(UserId)) of
{ok, User} ->
Req = cowboy_req:reply(200,
#{<<"content-type">> => <<"application/json">>},
jsx:encode(User),
Req0),
{ok, Req, State};
error ->
Req = cowboy_req:reply(404,
#{<<"content-type">> => <<"application/json">>},
jsx:encode(#{error => <<"Not found">>}),
Req0),
{ok, Req, State}
end.