Your first node
Tested on ROS 2 Jazzy · Ubuntu 24.04 · Python 3.12
By the end of this page you will have two programs talking to each other through ROS 2, and you will have proved it with numbers rather than hope.
This assumes ROS 2 is already installed. If it is not, the install guide is part 2, or follow the official installation docs and come back.
1. Check what you have
Section titled “1. Check what you have”printenv ROS_DISTROYou should see jazzy or humble. If you see nothing, ROS 2 is installed but not sourced
in this terminal:
source /opt/ros/jazzy/setup.bash2. Make a workspace
Section titled “2. Make a workspace”A workspace is just a folder with a src inside it. Nothing is registered anywhere.
mkdir -p ~/ros2_ws/srccd ~/ros2_ws/src3. Create a package
Section titled “3. Create a package”ros2 pkg create --build-type ament_python konjam_demo --dependencies rclpy std_msgsament_pythonmeans a pure Python package. There is alsoament_cmakefor C++.--dependencieswritesrclpyandstd_msgsintopackage.xmlfor you. You can add them later by hand; this just saves a step.
You now have this:
~/ros2_ws/src/konjam_demo/├── konjam_demo/ ← your Python code goes in here│ └── __init__.py├── resource/├── test/├── package.xml ← dependencies and metadata├── setup.cfg└── setup.py ← where you register commandsThe doubled konjam_demo/konjam_demo/ is not a mistake. The outer one is the package
folder, the inner one is the Python module.
4. Write the talker
Section titled “4. Write the talker”import rclpyfrom rclpy.node import Nodefrom std_msgs.msg import String
class Talker(Node): def __init__(self): super().__init__("talker") self.publisher = self.create_publisher(String, "chatter", 10) self.timer = self.create_timer(0.1, self.tick) self.count = 0
def tick(self): msg = String() msg.data = f"vanakkam {self.count}" self.publisher.publish(msg) self.get_logger().info(f'publishing "{msg.data}"') self.count += 1
def main(args=None): rclpy.init(args=args) node = Talker() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() if rclpy.ok(): rclpy.shutdown()
if __name__ == "__main__": main()Four things worth knowing:
super().__init__("talker")is the node name, which is what shows up inros2 node list. It is not the file name and not the command name. All three can differ, which is confusing exactly once.create_publisher(String, "chatter", 10)takes the message type, the topic name, and a queue depth. The10means “keep up to 10 messages for a subscriber that is briefly too slow”. It is a simplified quality-of-service setting, covered properly in part 3.create_timer(0.1, self.tick)callstickevery 0.1 seconds, so 10 Hz. ROS 2 does not want you writing your ownwhile Trueloop with asleep; the timer lets it manage timing and shutdown.rclpy.spin(node)hands control to ROS. It blocks and processes callbacks until the node is shut down.
5. Write the listener
Section titled “5. Write the listener”import rclpyfrom rclpy.node import Nodefrom std_msgs.msg import String
class Listener(Node): def __init__(self): super().__init__("listener") self.subscription = self.create_subscription( String, "chatter", self.on_message, 10 )
def on_message(self, msg): self.get_logger().info(f'heard "{msg.data}"')
def main(args=None): rclpy.init(args=args) node = Listener() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() if rclpy.ok(): rclpy.shutdown()
if __name__ == "__main__": main()The topic name "chatter" has to match the talker exactly. ROS 2 turns it into
/chatter for you.
6. Register the commands
Section titled “6. Register the commands”Open setup.py and find entry_points. Replace it with:
entry_points={ "console_scripts": [ "talker = konjam_demo.talker:main", "listener = konjam_demo.listener:main", ],},The format is command = module.file:function. The name on the left is what you type
after ros2 run konjam_demo. Forgetting this step is the single most common reason a
new package “does not exist”.
7. Build
Section titled “7. Build”cd ~/ros2_wscolcon build --packages-select konjam_demo --symlink-install--packages-selectbuilds only this package instead of everything in the workspace.--symlink-installlinks your Python files instead of copying them, so editingtalker.pytakes effect without rebuilding. For a Python package this is almost always what you want.
You may see a SetuptoolsDeprecationWarning about setup.py install. On current Ubuntu
this is noise from setuptools, not a problem with your package. Ignore it.
8. Run it
Section titled “8. Run it”ROS needs to know about your newly built package, which means sourcing the workspace:
cd ~/ros2_wssource install/setup.bashros2 run konjam_demo talkerYou should see:
[INFO] [1730000000.123456789] [talker]: publishing "vanakkam 0"[INFO] [1730000000.223456789] [talker]: publishing "vanakkam 1"Now open a second terminal. It knows nothing about any of this, so source both:
source /opt/ros/jazzy/setup.bashsource ~/ros2_ws/install/setup.bashros2 run konjam_demo listener[INFO] [1730000000.323456789] [listener]: heard "vanakkam 3"[INFO] [1730000000.423456789] [listener]: heard "vanakkam 4"The listener starts at whatever number the talker is on, not at zero. It missed the earlier ones because it was not running. That is normal, and worth understanding now: ROS topics are a live stream, not a queue of everything that ever happened.
9. Prove it from the outside
Section titled “9. Prove it from the outside”Stop the listener and look at the topic directly. You never needed to write a subscriber to do this:
ros2 topic list/chatter/parameter_events/rosoutros2 topic echo /chatterros2 topic hz /chatteraverage rate: 10.001 min: 0.099s max: 0.101s std dev: 0.00038s window: 11There is the 10 Hz from create_timer(0.1, ...), measured rather than assumed. When a
robot misbehaves later, ros2 topic hz is usually the fastest way to find out whether a
node has stopped publishing or is simply publishing too slowly.
ros2 topic info /chatter --verboseThat prints the message type, how many publishers and subscribers are attached, and the QoS settings on each side. Mismatched QoS is a common reason two nodes that look correct refuse to talk, which is part 3.
When it does not work
Section titled “When it does not work”Package 'konjam_demo' not found
: You did not source install/setup.bash in this terminal, or you are in the wrong
workspace. Every new terminal needs sourcing.
No executable found
: The entry_points in setup.py do not match what you typed, or you edited setup.py
and did not rebuild. Entry points are read at build time, so --symlink-install does
not save you here. Run colcon build again.
Talker runs, listener prints nothing
: Check the topic name matches on both sides. Run ros2 topic list while the talker is
up: if you see /chatter the publisher is fine, so the problem is the subscriber. Then
check ROS_DOMAIN_ID is the same in both terminals with printenv ROS_DOMAIN_ID —
nodes on different domain IDs cannot see each other at all, and this bites people on
shared machines and in containers.
ModuleNotFoundError: No module named 'konjam_demo'
: You are running from inside ~/ros2_ws/src where a folder shadows the installed
package. cd ~/ros2_ws and try again.
What you built
Section titled “What you built”Two processes, in separate terminals, that found each other with no address, no port and no configuration. That discovery is most of what ROS 2 gives you, and it is why the framework is worth the ceremony.
Part 2 covers installing ROS 2 properly, including the parts of the official guide that usually go wrong. Part 3 is topics in earnest: message types, queue depths, and why two correct-looking nodes sometimes will not talk.

Questions and corrections
Stuck on a step, or found a command that no longer works? Say so here and the page gets fixed. Other people hitting the same wall will find your comment.
The comment box is not switched on yet. Until it is, the video comments and the issue tracker both work.
No GitHub account? Comment on the video instead, or open an issue on the repo.