Skip to content

Guide list | Previous: Node.js | Next: Go

Python Binding Usage Guide

Contract-owning document for this chapter — the Python bindings spec covers it. This chapter shows that contract as working sample code.

This document explains how to use Core raw messaging through the zlink Python package. It supports Python 3.9 and later; the native runtime target for the current Core 0.9.0 wheel is Linux x86_64. Other operating systems or CPU architectures aren't treated as a supported target until a separate Core 0.9.0 candidate and clean-consumer verification are complete.

Installation And A First Round Trip

import zlink

with zlink.create_context() as ctx:
    with zlink.create_pair_socket(ctx) as sender:
        with zlink.create_pair_socket(ctx) as receiver:
            sender.bind("inproc://python-guide-pair")
            receiver.connect("inproc://python-guide-pair")

            sender.send().message(b"hello").submit_sync()
            received = zlink.create_received()  # the caller owns the receive storage.
            assert receiver.recv_into(received)
            with received:
                assert received.to_bytes_list() == [b"hello"]

The with block releases the native resources of the context and socket. See bindings/python/samples/pair_recv_sample.py for a TCP example across separate processes.

Message And Received

A send builder can accumulate multiple parts. Message.from_(value) builds a message independent from the caller's value. The receive result is held by Received; the native view under parts is only valid while its owner is open. To pass it to another task or hold onto it longer, copy it explicitly as shown below.

message = zlink.Message.from_(bytearray(b"payload"))  # builds a message detached from the caller's buffer.
with message:
    socket.send().message(message).submit_sync()

received = zlink.create_received()
socket.recv_into(received)  # returns False for a non-blocking call that allows no-data.
with received:
    parts = received.to_bytes_list()  # makes a snapshot to hand outside the owner.

A receive with RecvFlags.DONT_WAIT returns False when there's no message. Control APIs that directly return a pending value — timers, monitors — return None when there's no value.

HWM-managed sends provide a result-object submit() and synchronous submit_sync() terminals. submit() returns a SendSubmission (result(): OK|BACKPRESSURED, admitted(): awaitable) immediately; in async code, use await socket.send().message(message).submit().admitted() to wait for admission (already complete when result() is OK). It submits with DONTWAIT and settles admission from the socket completion queue. On a plain thread, submit_sync() blocks in Core until local admission.

Request provides submit_sync() to block until the reply and submit() to return a RequestSubmission (result(), admitted(), plus a reply() awaitable). When result() is OK you can await reply() directly; it settles from the socket completion queue. The reply is that terminal result, not DATA received separately.

Core owns retry after accepting a pre-admission operation; do not create a caller retry queue or resubmit its payload. The shared native ZLINK_OPT_PENDING_MAX_MSGS/BYTES caps cover pending SEND and REQUEST; no send-only pending names exist. Completion means local admission, not peer delivery or an application acknowledgement.

Canceling an asyncio Task can stop the Python waiter. Before Core submit, abort without calling Core; after Core accepts the payload, admission or request work may continue and the socket owner drains a late completion. Set stream.options.recv_mode to zlink.StreamRecvMode.RAW or .PACKET before bind/connect, then use recv_into or recv_packet_into respectively.

If a public poller owns zlink.PollEventFlag.POLLCOMPLETION for a socket, keep another thread calling wait() while a blocking request or awaitable is pending. wait() drains native completions and settles or cleans Python state; calling a blocking terminal between waits on the same thread can stall it.

DEALER And ROUTER

DEALER and ROUTER carry a raw RoutingId; a ROUTER request also carries an opaque ReplyToken. The reply builder requires both values from Received.

_, endpoint = tcp_endpoint()

with zlink.create_context() as ctx:
    with zlink.create_router_socket(ctx) as router_socket:
        with zlink.create_dealer_socket(ctx) as dealer_socket:
            with router_socket.monitor_open(
                zlink.MonitorEventMask.CONNECTION_READY
            ) as router_monitor:
                with dealer_socket.monitor_open(
                    zlink.MonitorEventMask.CONNECTION_READY
                ) as dealer_monitor:
                    dealer_socket.set_routing_id(b"REQ-CLIENT")
                    router_socket.bind(endpoint)
                    dealer_socket.connect(endpoint)
                    wait_connected(router_monitor, dealer_monitor)

            submission = (
                dealer_socket.request().message(b"ping").timeout(2.0).submit()
            )
            await asyncio.to_thread(respond, router_socket)
            reply = await submission.reply
            try:
                if [part.to_bytes() for part in reply] != [b"pong"]:
                    raise AssertionError("unexpected reply payload")
            finally:
                for part in reply:
                    part.close()
            print('[dealer-router/request-reply/async] send: "ping" -> recv: "pong"')

See request_reply_async_sample.py for the completion-backed awaitable and reply-token lifetime.

Routing ID And Errors

Fixed-length routing IDs are built with RoutingId.from_(bytes). Empty values and values exceeding the Core max length are rejected at input validation.

rid = zlink.RoutingId.from_(b"server-01")
try:
    socket.send().message(b"data").submit_sync()
except zlink.SubmitError as exc:
    if exc.result == zlink.SubmitResult.BACKPRESSURED:
        # handle back-pressure as application policy, after checking the result.
        pass
    else:
        raise

SubmitError, RequestError, RecvError, BindError, ConnectError, ConfigError, and CloseError are all ZlinkError subtypes and expose result, code, and native_errno.

Threading Notes

submit_sync() stops its calling thread while waiting for HWM admission. This is safe on a plain thread because only that thread waits. Calling it inside an asyncio event loop stops the entire loop, so other tasks and send completions cannot progress. In asyncio code, await the admitted()/reply() of the result object returned by submit().

Samples And Perf

The raw sample runner includes the following:

  • pair_recv_sample.py
  • dealer_router_recv_sample.py
  • request_reply_async_sample.py
  • pubsub_recv_sample.py
  • stream_recv_sample.py
  • stream_packet_recv_sample.py
  • monitor_recv_sample.py

The perf runner must specify which Core or wheel runtime to use. Compare the path and SHA-256 it prints against the candidate evidence.

ZLINK_LIBRARY_PATH=/absolute/path/to/libzlink.so \
  bindings/python/perf/run_benchmarks.sh --smoke --pattern PAIR \
  --duration 1 --msg-sizes 64 --transports inproc --runs 1

Smoke mode checks process lifecycle and the required RESULT row; it doesn't produce an official report.