Cookbook - How to Get Started

You might have come here to see some examples. Very well... But have a quick look at Examples! page too!

Start the Node

from pyrlang import Node, Atom, GeventEngine # or AsyncioEngine

def main():
    eng = GeventEngine()  # or AsyncioEngine
    node = Node(node_name="py@", cookie="COOKIE", engine=eng)

    fake_pid = node.register_new_process()

    # To be able to send to Erlang shell by name first give it a
    # registered name: `erlang:register(shell, self()).`
    # To see an incoming message in shell: `flush().`
              receiver=(Atom('erl@'), Atom('shell')),


if __name__ == "__main__":

Here event_engine is a pluggable adapter which allows Pyrlang to run both with gevent (GeventEngine) and asyncio-driven (AsyncioEngine) event loops. Pyrlang in this case performs mostly protocols handling, while event engine will open connections, start tasks and sleep asynchronously.

Connect nodes


You can initiate the connection from either Erlang or Python side automatically by sending to a remote name using tuple format {Name, Node} or sending to a remote pid (if you have it).

You can initiate the connection between nodes from Erlang side in a different way. To do this on Erlang side you can use net_adm:ping.


Also you could send a message to {Name, Node}, where Node is an atom like 'py@', and Name is a pid or some registered name, which exists on the Python side.

{Name, 'py@'} ! hello.

If the process exists on Python side, its inbox_ field (which will be a Gevent or Asyncio Queue) will receive your message.

Exiting a Pyrlang "Process"

A Pyrlang "process" can exit if:

  • you call Process.exit

  • you call Node.exit_process

  • you link to some other process and it exits

  • remote node calls erlang:exit with its pid

Because registering a process in the process dictionary introduces an extra reference to your object, be sure to tell it explicitly to unregister: call Process's method exit().

A more general way to handle both local (by pid or name) and remote processes (by pid) would be to use Node's method exit_process(). It can send exit messages to remotes too.

RPC call from Erlang

Python node has the special named process running, called 'rex', which is necessary for Erlang RPC to work. You can send an RPC call to Python from Erlang. In the following example Pyrlang.logger module has a tty function which will transparently pass all args to the print operator.

rpc:call('py@', 'Pyrlang.logger', 'tty', ["Hello World"]).


You do not need to import module to perform the call, this will be done by Rex.


Module and function name can be atoms, strings (non-unicode) or binaries.

Function call result or error will be sent back to the caller. In case of error, Erlang exit exception will be created with the exception value from Python.

Rex also supports gen_server style calls from Erlang:

gen_server:call({rex, 'py@'},
                {call, 'Pyrlang.logger', tty, ["Hello"], self()}).

Send from Python locally

You can send messages using the method Node.send(_sender, receiver, message), which can deliver messages locally or remotely.

node.send(sender=None,  # argument unused
          message=(123, 4.5678, [term.Atom('test')]))

Send from Python to a remote

You can send messages to a remote pid. Sender pid is unused and can be None. The node connection will be established automatically.


You can send messages to a remote named process, for this use tuple send format like {Name, Node}. For remote sends sender pid is REQUIRED, even if it is a fake pid (see example below how to create a fake pid).

To try this, open an Erlang shell and register shell with the name 'shell':

(erl@ 1> erlang:register(shell, self())

Now we can try and send the message from Python (node connection will be established automatically):

fake_pid = node.register_new_process(None)  # create a fake pid
          receiver=(Atom('erl@'), Atom('shell')),
(erl@ 2> flush().
Shell got hello
(erl@ 3>

Send to a Python object

A python object inherited from Process will be a Greenlet (i.e. running in parallel with the rest of the system). A process is able to register itself (optional) with a name and handle incoming messages.

Messages sent to a pid or name will be automatically routed to such a process and arrive into its self.inbox_. The Process base class will constantly call self.handle_inbox() so you can check the messages yourself.

class MyProcess(Process):
    def __init__(self) -> None:
        node.register_name(self, Atom('my_process'))  # optional

    def handle_one_inbox_message(self, msg):
        print("Incoming", msg)
%% Now sending from Erlang is easy:
%% Note that this is syntax for sending to atom names, not to pids!
(erl@ 1> {my_process, 'py@'} ! hello.

%% If you know the Python pid in Erlang (if you communicated it
%% from your Python node), then send directly to it:
(erl@ 1> PyProcessPid ! hello.

Remote Calculations on Python Node

Problem: While it is possible to subclass the Process class and implement a Erlang-like process, often existing Python code exposes just a functional API or a class which has to be created for the calculation to be performed. Often you would like to use some functional API without sending the results over the wire until they are ready.

Solution: A notebook-like remote execution API, where intermediate call results are stored in history log and can be referred by name or index.

There is helper Erlang module called py.erl, please use it and see Remote Calling Python from Erlang for an example.

See also

Example3 in Examples! demonstrates this.

Batch Remote Calculations on Python Node

Problem: Often you would like to use some functional API without sending the results over the wire until they are ready. Moreover sometimes you might want to run same batch on multiple nodes, this is possible now too.

Batch remote calculations API allows you to prebuild your calculation as a data structure on Erlang side and then execute it remotely on one or more Pyrlang nodes, sending you the final result. Intermediate call results are stored in history log and can be referred by name or index.

It is possible to apply the same batch of calls to multiple nodes.

See also

Example4 in Examples! demonstrates this.

Gen_server-like Processes

To have a Process descendant which responds to gen_server:call, inherit your class from GenServer. When calling GenServer constructor in your __init__ specify an additional parameter accepted_calls which is a list of strings.

Functions with these names will be mapped to incoming gen_server:call and their result will be transparently 'replied' back to the caller.

class MyProcess(GenServer):
    def __init__(self) -> None:
        GenServer.__init__(self, accepted_calls=['hello'])

    def hello(self):
        return self.pid_

When you perform a gen_server:call with an atom, the atom becomes Python method name:

# 1> gen_server:call(Pid, my_method)
# becomes a call to
class MyClass:
    def my_method(self):
        pass # return None -> atom 'undefined' in Erlang

When you perform a gen_server:call with a tuple where first element is an atom, the atom becomes Python method name, and following tuple elements become python *args.

# 1> gen_server:call(Pid, {my_method, 1000, "hello"})
# becomes a call to
class MyClass:
    def my_method(self, i: int, s: bytes):
        pass # return None -> atom 'undefined' in Erlang

Linking/Monitoring from Erlang to Python

See example6 in Examples! for demo on linking a Pyrlang "process" to Erlang process, then killing it and observing an EXIT signal coming back.

See example7 in Examples! for demo on a Pyrlang "process" monitoring a Erlang process, then killing it and observing the monitor message.

Linking/Monitoring from Python to Erlang

See example5 in Examples! for demo on how Python can link and monitor local and remote pids.


To link two processes in Pyrlang use Node's link() method.


To monitor a process in Pyrlang use Node's monitor_process() method.