Write a Minimal ROS2 Python Node

In this tutorial we’re going to write a minimal ROS2 Python node, with no extra functionality, just to see how to start writing any of your ROS2 program. This code will serve as a base structure, and then you’ll be able to add any ROS2 functionality you want: topics, services, parameters, and so on.

First we’ll see how to create a very minimal node, and then I’ll show you the recommended way to write all your nodes, using OOP.

(If you’re interested about writing a node in Cpp, check out the ROS2 Cpp node tutorial)

Write a minimal ROS2 Python Node

Code structure to start an rclpy Node

import rclpy
from rclpy.node import Node

def main(args=None):
    rclpy.init(args=args)
    node = Node('my_node_name')
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

This code will just start a ROS2 node, and wait until you kill the node.

Let’s see what each line does:

import rclpy
from rclpy.node import Node

We first import rclpy, the ROS2 Python library, and then from rclpy.node we import the Node module.

def main(args=None):
    rclpy.init(args=args)

To instantiate a node, you first have to initialize ROS communications. rclpy.init(args) will do that for you, with some arguments that you can pass when you launch the node (from command line or launch file).

    node = Node('my_node_name')

This line will create the node. The Node constructor takes at least one parameter: the name of the node. For example this could be “my_robot_driver”, “my_camera”.

Once you have created the node, you can use it to start ROS2 publishers, subscribers, services, get parameters, etc.

    rclpy.spin(node)

We have a node, but if we do nothing else the program will exit.

rclpy.spin(node) will pause the program execution here, waiting for you to request to kill the node (for example CTRL+C in the terminal). During this time, any thread/timer you’ve created in the node will continue to be executed. Also, spin will be able to call any callback function that you’ve defined for the node, allowing your node to communicate with other nodes.

When you request to kill the node, the spin function will exit, and any callback won’t be callable anymore.

    rclpy.shutdown()

This is the first thing that gets executed after you request to kill the node from the outside.

rclpy.shutdown() will basically shutdown what you started when you executed rclpy.init().

if __name__ == '__main__':
    main()

This is just required if you want to start the Python script directly from the folder where you have written it. But usually, you will add an “entry point” for your node when you set it up inside your ROS2 environment (see the next section). The entry point here is the main() function. It’s very important that you write all your code inside the main() function.

To resume, here is what happens in this minimal ROS2 program:

  • Start ROS communications.
  • Instantiate the node.
  • Making the node spin.
  • Shutdown ROS communications.

Alright, you have seen how to write a (basic but working) ROS2 node! Now let’s start it.

Install and run your ROS2 Python node

First, create a ROS2 Python package and a Python file inside it, so that you can write the code.

$ cd ~/ros2_ws/src
$ ros2 pkg create my_robot_tutorials --build-type ament_python
$ cd my_robot_tutorials/my_robot_tutorials
$ touch my_python_node.py

Then, write the previous code into “my_python_node.py”.

You can directly start the Python script with python3 my_python_node.py. However it’s better to install it so you can use ros2 run, start the node from a launch file, etc.

Add an entry point in the “setup.py” file at the root of your package:

...
entry_points={
    'console_scripts': [
        'minimal_python_node = my_robot_tutorials.my_python_node:main'
    ],
...

“minimal_python_node” will be the name of the executable, “my_python_node” is the name of the Python file, and “my_node_name” is the name of the node (written in the Python file).

Also, add <depend>rclpy</depend> after <buildtool_depend>ament_python</buildtool_depend> in your “package.xml” file.

Now, build your package.

$ cd ~/ros2_ws
$ colcon build --packages-select my_robot_tutorials

Open a new terminal, source your environment and start your node.

$ . ~/ros2_ws/install/setup.bash
$ ros2 run ros2_tutorials_py minimal_node

As we didn’t write anything special, nothing will be printed. Press CTRL+C at any moment to kill the node.

Now let’s improve this node and make it do something.

The right way to write a ROS2 Python node: with OOP

Code structure

Let’s first create a structure where we can actually write the functionalities we want to add. We’ll use OOP as is it the recommended way to write a node in ROS2, and it works pretty well.

import rclpy
from rclpy.node import Node

class MyNode(Node):
    def __init__(self):
        super().__init__('my_node_name')

def main(args=None):
    rclpy.init(args=args)
    node = MyNode()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

This structure will allow you to write all your node’s code in the class you created.

class MyNode(Node):
    def __init__(self):
        super().__init__('my_node_name')

Here we create a class which inherits from the rclpy Node class. Each ROS2 Python node that you’ll write will be a superset of “rclpy.node.Node”.

We call super() in the constructor, in order to initialize the Node object, and we pass the node name to it. After that, you’ll be able to create publishers, subscribers, and other ROS2 functionalities.

The main function stays the same, but instead of node = Node(), we have node = MyNode(). We simply instantiate the custom node we’ve written, and then make it spin.

Add functionalities to the node – A simple example

Here we’ll do something basic: print “Hello ROS2” at 5Hz. We’ll add some code to our minimal structure.

import rclpy
from rclpy.node import Node

class MyNode(Node):
    def __init__(self):
        super().__init__('my_node_name')
        self.create_timer(0.2, self.timer_callback)

    def timer_callback(self):
        self.get_logger().info("Hello ROS2")

def main(args=None):
    rclpy.init(args=args)
    node = MyNode()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Let’s break the new stuff down line by line.

    def __init__(self):
        super().__init__('my_node_name')
        self.create_timer(0.2, self.timer_callback)

Inside the constructor, just after we initialize the node, we create a timer with the create_timer() method from the inherited Node class. You have to pass 2 arguments: the duration between 2 callbacks (in seconds), and the timer function.

    def timer_callback(self):
        self.get_logger().info("Hello ROS2")

The timer_callback() method is part of the MyNode class.

Inside, we use another inherited Node method: get_logger().info(). This will print a ROS2 log on the terminal.

Note: as you can see, the main function stays the same. This is great: whenever you make changes to your node, you actually don’t need to change anything in the main.

So, what will this program do?

  • When the node is created, the timer starts.
  • Then the node spins. This allows the timer to continue working.
  • When you press CTRL+C in the terminal, the spinning will end (and the timer ends).
  • Finally, the program exits and the node is destroyed.

To see the result, compile your package again and start the node with ros2 run.

$ ros2 run my_robot_tutorials minimal_python_node
[INFO] [my_node_name]: Hello ROS2
[INFO] [my_node_name]: Hello ROS2
[INFO] [my_node_name]: Hello ROS2
[INFO] [my_node_name]: Hello ROS2
[INFO] [my_node_name]: Hello ROS2

For reference: ROS2 Python node without OOP (not recommended)

Just so you can have a more complete view on how to write functionalities for nodes, here is the same example without OOP.

import rclpy
from rclpy.node import Node

def timer_callback():
    global node
    node.get_logger().info("Hello ROS2")

def main(args=None):
    rclpy.init(args=args)
    global node
    node = Node('my_node_name')
    node.create_timer(0.2, timer_callback)
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

This code will do the same as before.

Note however that we added functionality code inside the main function, and we also used the global keyword to be able to access the node from the timer callback.

This way to write code may work for small programs, but as you scale your application, using OOP will be more readable, and bring much more modularity.

Also, if you look at the code of robots using ROS2, you’ll see that most of them are going the OOP way. This convention is not only a better practice, it will also help you work and adapt quicker with other people on a ROS2 project.

Going further with ROS2 Python nodes

In this tutorial you’ve seen how to write a very basic and minimal ROS2 Python node with rclpy.

It’s important that you understand the basic program structure to create a node, because all the ROS2 programs you’ll need in your application will have the same foundation.

There are many more things next to come concerning nodes themselves: lifecycled nodes, components, intra-process communication, etc. Once you have a good understanding of the basics you’ll be able to work on those.

Right now, you can start experimenting with topics, services, actions, parameters, etc., and learn how to use them from within your node’s code. Those are the most useful ROS2 core functionalities, you’ll use them in every program you write.

FREE Mini-Course

Don't miss this opportunity:

ROS2 Core Concepts in 1h


>> ROS2 Core Concepts Overview in 1h <<