Jul 11, 2026 · ros2 · python · getting-started
I rewrote my ROS 2 node in 8 lines of Python
A ROS 2 publisher/subscriber is a class, an __init__, a spin loop, and a launch file. The same robot in HORUS is eight lines — and the messages never serialize.
If you have written a ROS 2 node in Python, you know the shape of it: subclass Node,
call super().__init__('name'), create a publisher, create a subscriber with a callback,
then hand yourself to rclpy.spin. Then a second file — the launch file — to actually run it.
Here is the same sensor→controller robot in HORUS, in Python, in full:
import horus
def sensor_tick(node):
node.send("sensor.data", {"position": sensor_tick.pos, "velocity": 0.5})
sensor_tick.pos += 0.01
sensor_tick.pos = 0.0
def controller_tick(node):
s = node.recv("sensor.data")
if s is not None:
node.send("motor.cmd", {"voltage": (1.0 - s["position"]) * 0.5})
horus.run(
horus.Node(name="sensor", pubs=["sensor.data"], tick=sensor_tick, rate=1000),
horus.Node(name="ctrl", subs=["sensor.data"], pubs=["motor.cmd"], tick=controller_tick, rate=1000),
)No class boilerplate. No separate launch file. No colcon build. horus.run(...) is the
launch file. And both nodes tick at 1 kHz — set by rate=1000, not by a wall-clock sleep
you tuned by hand.
The part that isn't just less typing
The interesting difference is underneath. In ROS 2, sensor.data crosses the process
boundary through DDS — it gets serialized, shipped, and deserialized every message. The nearest
published figure for the default DDS stack is REP 2014's ~5 µs for a small message, and larger
payloads and stricter QoS only go up from there. In HORUS, that Python detector and the
controller share the same shared-memory ring buffer, enabling zero-copy messaging across languages and processes. The message is the type's memory layout; there is no serialization step at all. The same topic that costs microseconds in ROS 2
moves in nanoseconds here.
That is also why the Python node and a C++ controller can subscribe to the same topic with zero glue: all three languages agree on the wire format because the wire format is just the bytes. This brevity isn’t just syntax sugar — it reflects a fundamentally simpler runtime model built around deterministic scheduling and shared memory. Learn more about what zero-copy messaging means for robotics.
When this matters (and when it doesn't)
If you are prototyping and your loop runs at 10 Hz, ROS 2's overhead is invisible and its ecosystem is enormous — use it. HORUS earns its keep when the timing is real: a 1 kHz motor loop, a camera→inference→actuation pipeline you want in one process, or a controller that must not miss its deadline. There, a messaging layer measured in nanoseconds rather than microseconds — and, more to the point, one whose worst case you can bound — stops being a benchmark curiosity and starts being the difference between a robot that holds its control loop and one that doesn't. The numbers, and how they're measured, are worth reading before you believe any of them.
If you're coming from ROS 2, the migration guide maps every concept — Node, Topic, Service, Action, tf2, QoS — to its HORUS equivalent.
The whole thing is open-source and Apache-2.0. If the 8-line version made you curious, the code above is a real example in the repo.