Skip to main content

PyZeROS

Requirements Compatibility Tests Matrix
python
license
ros
zenoh
linux
jazzy
lyrical

Python-only ROS 2 API. No ROS installation, no rclpy, no message compilation. Just pip install and talk to your favorite ROS network.

Built on Zenoh, asyncio-for-robotics, and ros2-pyterfaces.

import asyncio, pyzeros
import asyncio_for_robotics as afor
from ros2_pyterfaces.cyclone.all_msgs import String

@afor.scoped
async def main():
    sub = pyzeros.Sub(String, "chatter")
    async for msg in sub.listen_reliable():
        print(msg.data)

with pyzeros.auto_context(node="listener", namespace="/demo"):
    asyncio.run(main())

Features:

  • Topics and services, fully interoperable with ROS 2 nodes.
  • asyncio execution model. No callbacks, no spinners, just Python.
  • Define ROS messages in Python with ros2_pyterfaces.
  • Resource lifecycle via sessions and scopes.

[!NOTE] Experimental. Actions and zero-copy are planned.


ROS 2 interop

PyZeROS talks to ROS 2 through Zenoh. The ROS 2 side must use rmw_zenoh_cpp:

export RMW_IMPLEMENTATION=rmw_zenoh_cpp

Both sides must share the same ROS_DOMAIN_ID (defaults to 0) and be on the same Zenoh network. In practice this means running a Zenoh router and configuring both sides as clients to it, or using a peer-to-peer Zenoh config for DDS-like multicast discovery.

See the rmw_zenoh docs for router setup and configuration.

Install

PyZeROS is a normal Python package. Add it as a dependency to your project with your tool of choice. No colcon, no workspace, no overlay.

pip install pyzeros

From source

git clone https://github.com/2lian/pyzeros2
cd pyzeros2
pixi install
pixi run router   # start a local Zenoh router
pixi run example  # run the minimal subscriber

Tutorial

This mirrors the official ROS 2 tutorials, but without C++, build systems, or boilerplate.

1. Publisher and subscriber

The ROS 2 tutorial for this is here. Below is the PyZeROS equivalent.

Publisher:

import asyncio
import pyzeros
import asyncio_for_robotics as afor
from ros2_pyterfaces.cyclone.all_msgs import String

@afor.scoped
async def main():
    pub = pyzeros.Pub(String, "chatter")
    counter = 0
    async for _ in afor.Rate(2).listen():
        pub.publish(String(data=f"Hello World: {counter}"))
        print(f"Publishing: Hello World: {counter}")
        counter += 1

with pyzeros.auto_context(node="talker", namespace="/demo"):
    asyncio.run(main())

Subscriber:

import asyncio
import pyzeros
import asyncio_for_robotics as afor
from ros2_pyterfaces.cyclone.all_msgs import String

@afor.scoped
async def main():
    sub = pyzeros.Sub(String, "chatter")
    async for msg in sub.listen_reliable():
        print(f"I heard: {msg.data}")

with pyzeros.auto_context(node="listener", namespace="/demo"):
    asyncio.run(main())

That's it. No rclpy.init(), no spin(), no executor. The async for loop is the executor.

auto_context creates and binds the context-local session, while @afor.scoped cleans up its async resources. See asyncio-for-robotics for details on scopes and sessions.

2. Service and client

The ROS 2 tutorial for this is here. Below is the PyZeROS equivalent.

Server:

import asyncio
import pyzeros
import asyncio_for_robotics as afor
from ros2_pyterfaces.cyclone.all_srvs import AddTwoInts

@afor.scoped
async def main():
    server = pyzeros.Server(AddTwoInts, "add_two_ints")
    print("Service ready.")
    async for responder in server.listen_reliable():
        result = responder.request.a + responder.request.b
        responder.response.sum = result
        responder.send()
        print(f"{responder.request.a} + {responder.request.b} = {result}")

with pyzeros.auto_context(node="add_server", namespace="/demo"):
    asyncio.run(main())

Client:

import asyncio
import pyzeros
import asyncio_for_robotics as afor
from ros2_pyterfaces.cyclone.all_srvs import AddTwoInts

@afor.scoped
async def main():
    client = pyzeros.Client(AddTwoInts, "add_two_ints")
    await client.wait_for_service()
    response = await client.call_async(AddTwoInts.Request(a=2, b=3))
    print(f"Result: {response.sum}")

with pyzeros.auto_context(node="add_client", namespace="/demo"):
    asyncio.run(main())

Services follow the same async for pattern as topics. The server yields Responder objects: read responder.request, fill responder.response, call responder.send().

3. Custom messages

The ROS 2 tutorial for this is here. In ROS 2 this involves .msg files, CMake, and colcon build. In PyZeROS, it's a dataclass.

ros2_pyterfaces provides two backends for message definitions:

Backend Import Speed Compatibility
cyclone ros2_pyterfaces.cyclone Good Full ROS 2 interop
cydr ros2_pyterfaces.cydr Faster Slightly less compatible with edge cases

Both backends ship pre-built standard messages (all_msgs, all_srvs) and let you define your own. You can find message definitions for multiple ROS distros: Humble, Jazzy, Kilted, Lyrical.

[!IMPORTANT] For ROS 2 interop, the typename and field names must match the ROS message definition exactly.

Defining a message:

from dataclasses import dataclass, field
from ros2_pyterfaces.cyclone import idl, all_msgs

@dataclass
class MyStatus(idl.IdlStruct, typename="my_package/msg/MyStatus"):
    header: all_msgs.Header = field(default_factory=all_msgs.Header)
    temperature: idl.types.float64 = 0.0
    labels: idl.types.sequence[str] = field(default_factory=list)
    active: bool = False

Use it like any other message:

pub = pyzeros.Pub(MyStatus, "status")
pub.publish(MyStatus(temperature=36.5, labels=["sensor_a"], active=True))

Defining a service:

from dataclasses import dataclass
from ros2_pyterfaces.cyclone.idl import IdlStruct, make_idl_service

@dataclass
class Request(IdlStruct, typename="my_package/srv/Calibrate_Request"):
    target: str = ""

@dataclass
class Response(IdlStruct, typename="my_package/srv/Calibrate_Response"):
    success: bool = False

Calibrate = make_idl_service(Request, Response)

See ros2_pyterfaces for the full type system.


Putting it together

A more realistic node combining topics and services with proper resource management:

import asyncio
from contextlib import suppress

import asyncio_for_robotics as afor
import pyzeros
from ros2_pyterfaces.cyclone.all_msgs import String
from ros2_pyterfaces.cyclone.all_srvs import Trigger

@afor.scoped
async def main():
    tg = afor.Scope.current().task_group
    tg.create_task(publisher())
    tg.create_task(listener())
    tg.create_task(serve_trigger())
    await asyncio.Future()

@afor.scoped
async def publisher():
    pub = pyzeros.Pub(String, "heartbeat")
    counter = 0
    async for _ in afor.Rate(1).listen():
        pub.publish(String(data=f"alive #{counter}"))
        counter += 1

async def listener():
    sub = pyzeros.Sub(String, "commands")
    async for msg in sub.listen_reliable():
        print(f"Command: {msg.data}")

async def serve_trigger():
    server = pyzeros.Server(Trigger, "reset")
    async for responder in server.listen_reliable():
        print("Reset triggered!")
        responder.response.success = True
        responder.response.message = "done"
        responder.send()

if __name__ == "__main__":
    with pyzeros.auto_context(node="my_robot", namespace="/robot"):
        with suppress(KeyboardInterrupt):
            asyncio.run(main())

What this gives you over standard ROS 2 Python:

  • Scoped cleanup: @afor.scoped closes all publishers, subscribers, servers, and rates when the function exits. No dangling resources, no manual destroy_* calls.
  • TaskGroup structure: Tasks run concurrently inside the scope's TaskGroup. If one crashes, the others are cancelled and the error propagates cleanly. In standard ROS 2, a crashed callback silently dies.
  • Session resolution: auto_context binds a session (node identity + transport) for the block. Every Pub, Sub, Client, Server created inside auto-resolves to that session. No passing self.node around.
  • Thread safety by default: Everything runs on one asyncio event loop. No GIL juggling, no executor threads, no callback reentrancy bugs.

For the scope and session system in detail, see asyncio-for-robotics.


Examples

[!NOTE] Those examples are meant to run out-of-the-box with an installation from source using pixi. With just a pip install you'll need to setup your environment and ROS yourself.

Examples live under pyzeros.examples.*:

Example Run Description
example.py pixi run example Minimal subscriber
basic_usage.py pixi run python -m pyzeros.examples.basic_usage Repeater with custom message
demo.py pixi run demo Ring of async tasks
custom_msgs.py pixi run python -m pyzeros.examples.custom_msgs Python-defined JointState

Inspect from the ROS 2 side:

pixi run -e ros ros2 topic list
pixi run -e ros ros2 topic echo /demo/chatter std_msgs/msg/String

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyzeros-0.1.0.tar.gz (45.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyzeros-0.1.0-py3-none-any.whl (44.1 kB view details)

Uploaded Python 3

File details

Details for the file pyzeros-0.1.0.tar.gz.

File metadata

  • Download URL: pyzeros-0.1.0.tar.gz
  • Upload date:
  • Size: 45.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.22

File hashes

Hashes for pyzeros-0.1.0.tar.gz
Algorithm Hash digest
SHA256 302f55b8d3ddf2f01c5e0aeee39ab5725a537a8fd8547eee5c8b44fd54b81c69
MD5 5a4abbf159d8a915ef35a495498a4f15
BLAKE2b-256 578cc93e12abb13b2bd318a6d854193a0b92d6354844169b56dbdd253d3fea3e

See more details on using hashes here.

File details

Details for the file pyzeros-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pyzeros-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 44.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.22

File hashes

Hashes for pyzeros-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2a112618541c448c80511f8fe69284a2c4258da98349fdea5b720cb8213e2d99
MD5 a9259b8607f206bd8d8a88f70a40456f
BLAKE2b-256 53d97116f7c0832a66dec86f6cb92d4c2e3f6794248e4a31fde396560e605077

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page