Maker.io main logo

Intro to ROS Part 3: Nodes, Topics, Publishers, and Subscribers (Python)

2025-10-02 | By ShawnHymel

Raspberry Pi SBC

In the previous episodes of our ROS 2 tutorial series, we covered the theory behind nodes, topics, and services and demonstrated how to inspect and debug ROS applications using the command line. Now it’s time to write your first ROS 2 nodes in Python. This tutorial will walk you through creating a publisher and subscriber from scratch using ROS 2’s Python client library, rclpy.

By the end of this guide, you’ll know how to:

  • Create a ROS 2 Python package
  • Write a publisher that sends messages on a topic
  • Write a subscriber that listens to those messages
  • Build and run your package with colcon
  • Visualize node connections using rqt_graph

The Docker image and code for this series can be found here: https://github.com/ShawnHymel/introduction-to-ros

Understanding ROS 2 Nodes and Topics

Intro to ROS Part 3: Nodes, Topics, Publishers, and Subscribers (Python)

In ROS 2, applications are composed of nodes: independent processes that handle specific tasks like reading sensor data, processing algorithms, or controlling hardware. Each node is typically created as a class that subclasses the Node base class from the rclpy library. This structure allows you to encapsulate functionality cleanly and leverage object-oriented programming principles.

Nodes can communicate with each other using topics, which implement a publish/subscribe messaging model. A publisher sends data to a named topic, while one or more subscribers listen to that topic and receive the data. This decoupling of senders and receivers makes it easy to scale and maintain your robot software.

For example, imagine a temperature sensor node that publishes readings to a /temperature topic. Any node that needs temperature data, such as a logging or display node, can simply subscribe to that topic. This model promotes loose coupling, meaning publishers don’t need to know which nodes are subscribed, and subscribers don’t need to know the details of how messages are produced.

When you create a publisher or subscriber, you specify the topic name, the message type (such as std_msgs/msg/String), and an optional queue size or Quality of Service (QoS) profile to control message delivery behavior.

Setting Up the ROS 2 Environment

We’ll continue using the Docker-based ROS 2 environment configured in earlier episodes. If you’re on Windows, run the following command in PowerShell to launch the container:

Copy Code
docker run --rm -it -e PUID=$(wsl id -u) -e PGID=$(wsl id -g) -p 22002:22 -p 3000:3000 -v "${PWD}\workspace:/config/workspace" env-ros2

Navigate to localhost:3000 in your browser to open the VS Code environment preconfigured for ROS 2 development. You’ll be working inside the /config/workspace directory, which is mapped to your host and stores your custom packages.

If ros2 and colcon commands don’t work, ensure your ROS 2 environment is sourced:

Copy Code
source /opt/ros/jazzy/setup.bash

Creating a Python Package

A ROS 2 workspace is simply a directory where you store and build your ROS packages. It typically contains a src/ folder where your actual packages live. When you run colcon build, it creates a build/ directory with intermediate files, an install/ directory containing the built packages, and a log/ directory for diagnostics. This structure allows ROS 2 to manage multiple packages simultaneously and makes it easy to isolate your development environment. The modular design also encourages clean organization and reuse across projects. Keeping your workspace organized becomes more important as your robot software grows in complexity.

Create your ROS 2 workspace structure:

Copy Code
cd /config/workspace
 mkdir src
 cd src
 ros2 pkg create --build-type ament_python my_py_pkg

This generates a basic Python package with the following key elements:

  • my_py_pkg/: your Python module
  • resource/: contains an empty file named after the package (used for discovery)
  • package.xml: ROS 2 package manifest (lists dependencies)
  • setup.cfg and setup.py: Python build metadata

Writing the Publisher Node

Create a file named minimal_publisher.py inside my_py_pkg/my_py_pkg/:

Copy Code
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from example_interfaces.msg import String
class MinimalPublisher(Node):
 """Publisher example that periodically sends out a string"""
def __init__(self): 
"""Constructor"""
 # Call the Node class constructor with the node name
 super().__init__('minimal_publisher')
 # Create a publisher object 
self._publisher = self.create_publisher(String, 'my_topic', 10)
# Periodically call method
self._timer = self.create_timer(0.5, self._timer_callback)
 # Counter for messages sent
self._counter = 0
def _timer_callback(self):
 """Publishes a simple message to topic"""
 # Fill out String message
 msg = String()
 msg.data = f"Hello world: {self._counter}"
 # Publish message to topic
 self._publisher.publish(msg)
 self.get_logger().info(f"Publishing: {msg.data}")
 # Increment counter
 self._counter += 1
def main(args=None):
 """Main entrypoint"""
 # Initialize and run node
try:
 rclpy.init()
 node = MinimalPublisher()
 rclpy.spin(node)
# Catch ctrl+c or shutdown request
except (KeyboardInterrupt, ExternalShutdownException): 
pass
 # Destroy node (now) and gracefully exit 
finally: 
if node is not None: 
node.destroy_node() 
if rclpy.ok():
 rclpy.shutdown()
if __name__ == '__main__': 
main()

In the call to create_publisher() and create_subscription(), you’ll notice the number 10 passed in as the third argument. This value defines the queue depth, which is a simple form of quality of service (QoS). In more advanced use cases, ROS 2 supports a wide range of QoS policies that control how messages are delivered under various conditions, such as lossy networks or when nodes are late to start. For now, the default settings are sufficient, but understanding QoS will be essential when working with real robots, especially those using wireless communication or with timing-sensitive data like video or sensor streams. You can read more about QoS settings here: https://docs.ros.org/en/jazzy/Concepts/Intermediate/About-Quality-of-Service-Settings.html

Update setup.py

Now register this script as an executable by editing setup.py. In the entry_points variable, add the following:

Copy Code
entry_points={
 'console_scripts': [
 'minimal_publisher = my_py_pkg.minimal_publisher:main' 
 ], 
}

Update package.xml

In your package, package.xml contains information used by the ROS system: metadata about the package and dependencies on other ROS packages. Every time you include another ROS package in your code, you should update the package.xml in your package.

This is often considered a “best practice” as your code may still compile and run without it! But you might run into bugs later on, as ROS uses this dependency information when running packages.

Add the following dependencies after the <buildtool_depend>  line:

Copy Code
<depend>rclpy</depend>
<depend>example_interfaces</depend>

Building and Running the Publisher

Return to the workspace root and build your package:

Copy Code
colcon build --packages-select my_py_pkg

Source the new environment so ROS can find the package:

Copy Code
source install/setup.bash
 

Now run the publisher node:

Copy Code
ros2 run my_py_pkg minimal_publisher

Open another terminal and inspect the topic:

Copy Code
ros2 topic list 
ros2 topic echo /my_topic

You should see the "Hello world" messages being published in real time.

Writing the Subscriber Node

Now let’s create a subscriber node to receive and print these messages. Create a file named minimal_subscriber.py in the same directory:

Copy Code
import rclpy 
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node 
from example_interfaces.msg import String
class MinimalSubscriber(Node):
 """Subscriber example that prints messages to the console""" 
def __init__(self): 
"""Constructor""" 
# Call the Node class constructor with the node name 
super().__init__('minimal_subscriber')
# Create a subscription object
self._subscription = self.create_subscription( 
String,
'my_topic', 
self._listener_callback, 
10
) 
def _listener_callback(self, msg):
 """Prints message to the console""" 
self.get_logger().info(f"Received message: {msg.data}") 
def main(args=None):
 """Main entrypoint""" 
# Initialize and run node
try: 
rclpy.init()
 node = MinimalSubscriber() 
rclpy.spin(node)
 # Catch ctrl+c or shutdown request
except (KeyboardInterrupt, ExternalShutdownException): 
pass 
# Destroy node (now) and gracefully exit
finally: 
if node is not None:
 node.destroy_node()
 if rclpy.ok():
 rclpy.shutdown()
 if __name__ == '__main__': 
main()

Add it to the setup.py entry points:

Copy Code
 'minimal_subscriber = my_py_pkg.minimal_subscriber:main',

Rebuild your package:

Copy Code
colcon build --packages-select my_py_pkg 
source install/setup.bash

In one terminal, run:

Copy Code
ros2 run my_py_pkg minimal_publisher

In another terminal:

Copy Code
ros2 run my_py_pkg minimal_subscriber

You should see messages appear on both terminals, showing the subscriber successfully receiving data from the publisher.

Intro to ROS Part 3: Nodes, Topics, Publishers, and Subscribers (Python)

Visualizing with rqt_graph

Launch the ROS graph visualization tool:

Copy Code
rqt_graph

This shows a graphical view of nodes and topics. You’ll see minimal_publisher and minimal_subscriber connected by the my_topic line.

Intro to ROS Part 3: Nodes, Topics, Publishers, and Subscribers (Python)

Shutting Down and Next Steps

Use Ctrl+C to stop all nodes. To stop the Docker container entirely, use Docker Desktop or the CLI (from another terminal):

Copy Code
docker ps
docker stop <CONTAINER_ID>

You’ve now built and connected your first ROS 2 nodes using Python. This foundation will support more advanced interactions in future lessons, including parameters, services, and hardware integration. Up next, we’ll implement a service-server interaction using the same framework.

Stay tuned!

Mfr Part # 28009
EXPERIENTIAL ROBOTICS PLATFORM (
SparkFun Electronics
$984.91
View More Details
Mfr Part # SC0194(9)
SBC 1.5GHZ 4 CORE 4GB RAM
Raspberry Pi
$451.61
View More Details
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.