diff --git a/source/How-To-Guides/Migrating-from-ROS1.rst b/source/How-To-Guides/Migrating-from-ROS1.rst
index 91e298d1af..6b095ca63f 100644
--- a/source/How-To-Guides/Migrating-from-ROS1.rst
+++ b/source/How-To-Guides/Migrating-from-ROS1.rst
@@ -12,6 +12,7 @@ If you are new to porting between ROS 1 and ROS 2, it is recommended to read thr
Migrating-from-ROS1/Migrating-Interfaces
Migrating-from-ROS1/Migrating-CPP-Package-Example
Migrating-from-ROS1/Migrating-CPP-Packages
+ Migrating-from-ROS1/Migrating-Python-Package-Example
Migrating-from-ROS1/Migrating-Python-Packages
Migrating-from-ROS1/Migrating-Launch-Files
Migrating-from-ROS1/Migrating-Parameters
diff --git a/source/How-To-Guides/Migrating-from-ROS1/Migrating-CPP-Package-Example.rst b/source/How-To-Guides/Migrating-from-ROS1/Migrating-CPP-Package-Example.rst
index 68038da8e0..5cdb8ff0cc 100644
--- a/source/How-To-Guides/Migrating-from-ROS1/Migrating-CPP-Package-Example.rst
+++ b/source/How-To-Guides/Migrating-from-ROS1/Migrating-CPP-Package-Example.rst
@@ -43,7 +43,7 @@ The files have the following content:
talker
0.0.0
talker
- Brian Gerkey
+ Brian Gerkey
Apache-2.0
catkin
roscpp
@@ -304,7 +304,7 @@ Your ``package.xml`` now looks like this:
talker
0.0.0
talker
- Brian Gerkey
+ Brian Gerkey
Apache-2.0
ament_cmake
rclcpp
diff --git a/source/How-To-Guides/Migrating-from-ROS1/Migrating-Python-Package-Example.rst b/source/How-To-Guides/Migrating-from-ROS1/Migrating-Python-Package-Example.rst
new file mode 100644
index 0000000000..d4230c615c
--- /dev/null
+++ b/source/How-To-Guides/Migrating-from-ROS1/Migrating-Python-Package-Example.rst
@@ -0,0 +1,863 @@
+Migrating a Python Package Example
+==================================
+
+This guide shows how to migrate an example Python package from ROS 1 to ROS 2.
+
+.. contents:: Table of Contents
+ :depth: 2
+ :local:
+
+Prerequisites
+-------------
+
+You need a working ROS 2 installation, such as :doc:`ROS {DISTRO} <../../Installation>`.
+
+The ROS 1 code
+--------------
+
+You won't be using `catkin `__ in this guide, so you don't need a working ROS 1 installation.
+You are going to use ROS 2's build tool `Colcon `__ instead.
+
+This section gives you the code for a ROS 1 Python package.
+The package is called ``talker_py``, and it has one node called ``talker_py_node``.
+To make it easier to run Colcon later, these instructions make you create the package inside a `Colcon workspace `__,
+
+First, create a folder at ``~/ros2_talker_py`` to be the root of the Colcon workspace.
+
+.. tabs::
+
+ .. group-tab:: Linux
+
+ .. code-block:: bash
+
+ mkdir -p ~/ros2_talker_py/src
+
+ .. group-tab:: macOS
+
+ .. code-block:: bash
+
+ mkdir -p ~/ros2_talker_py/src
+
+ .. group-tab:: Windows
+
+ .. code-block:: bash
+
+ md \ros2_talker_py\src
+
+Next, create the files for the ROS 1 package.
+
+.. tabs::
+
+ .. group-tab:: Linux
+
+ .. code-block:: bash
+
+ cd ~/ros2_talker_py
+ mkdir -p src/talker_py/src/talker_py
+ mkdir -p src/talker_py/scripts
+ touch src/talker_py/package.xml
+ touch src/talker_py/CMakeLists.txt
+ touch src/talker_py/src/talker_py/__init__.py
+ touch src/talker_py/scripts/talker_py_node
+ touch src/talker_py/setup.py
+
+ .. group-tab:: macOS
+
+ .. code-block:: bash
+
+ cd ~/ros2_talker_py
+ mkdir -p src/talker_py/src/talker_py
+ mkdir -p src/talker_py/scripts
+ touch src/talker_py/package.xml
+ touch src/talker_py/CMakeLists.txt
+ touch src/talker_py/src/talker_py/__init__.py
+ touch src/talker_py/scripts/talker_py_node
+ touch src/talker_py/setup.py
+
+ .. group-tab:: Windows
+
+ .. code-block:: bash
+
+ cd \ros2_talker_py
+ md src\talker_py\src\talker_py
+ md src\talker_py\scripts
+ type nul > src\talker_py\package.xml
+ type nul > src\talker_py\CMakeLists.txt
+ type nul > src\talker_py\src\talker_py\__init__.py
+ type nul > src\talker_py\scripts/talker_py_node
+ type nul > src\talker_py\setup.py
+
+Put the following content into each file.
+
+``src/talker_py/package.xml``:
+
+.. code-block:: xml
+
+
+
+
+ talker_py
+ 1.0.0
+ The talker_py package
+ Brian Gerkey
+ BSD
+
+ catkin
+
+ rospy
+ std_msgs
+
+
+``src/talker_py/CMakeLists.txt``:
+
+.. code-block:: cmake
+
+ cmake_minimum_required(VERSION 3.0.2)
+ project(talker_py)
+
+ find_package(catkin REQUIRED)
+
+ catkin_python_setup()
+
+ catkin_package()
+
+ catkin_install_python(PROGRAMS
+ scripts/talker_py_node
+ DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
+ )
+
+``src/talker/src/talker_py/__init__.py``:
+
+.. code-block:: Python
+
+ import rospy
+ from std_msgs.msg import String
+
+ def main():
+ rospy.init_node('talker')
+ pub = rospy.Publisher('chatter', String, queue_size=10)
+ rate = rospy.Rate(10) # 10hz
+ while not rospy.is_shutdown():
+ hello_str = "hello world %s" % rospy.get_time()
+ rospy.loginfo(hello_str)
+ pub.publish(hello_str)
+ rate.sleep()
+
+``src/talker_py/scripts/talker_py_node``:
+
+.. code-block:: Python
+
+ #!/usr/bin/env python
+
+ import talker_py
+
+ if __name__ == '__main__':
+ talker_py.main()
+
+``src/talker_py/setup.py``:
+
+.. code-block:: Python
+
+ from setuptools import setup
+ from catkin_pkg.python_setup import generate_distutils_setup
+
+ setup_args = generate_distutils_setup(
+ packages=['talker_py'],
+ package_dir={'': 'src'}
+ )
+
+ setup(**setup_args)
+
+This is the complete ROS 1 Python package.
+
+Migrate the ``package.xml``
+---------------------------
+
+When migrating packages to ROS 2, migrate the build system files first so that you can check your work by building and running code as you go.
+Always start by migrating your ``package.xml``.
+
+First, ROS 2 does not use ``catkin``.
+Delete the ```` on it.
+
+.. code-block::
+
+
+ catkin
+
+
+Next, ROS 2 uses ``rclpy`` instead of ``rospy``.
+Delete the dependency on ``rospy``.
+
+.. code-block::
+
+
+ rospy
+
+
+Replace it with a new dependency on ``rclpy``.
+
+.. code-block:: xml
+
+ rclpy
+
+Add an ```` section to tell ROS 2's build tool `Colcon `__ that this is an ``ament_python`` package instead of a ``catkin`` package.
+
+.. code-block:: xml
+
+
+ ament_python
+
+
+
+Your ``package.xml`` is fully migrated.
+It should now look like this:
+
+.. code-block:: xml
+
+
+
+
+ talker_py
+ 1.0.0
+ The talker_py package
+ Brian Gerkey
+ BSD
+
+ rclpy
+ std_msgs
+
+
+ ament_python
+
+
+
+Delete the ``CMakeLists.txt``
+-----------------------------
+
+Python packages in ROS 2 do not use CMake, so delete the ``CMakeLists.txt``.
+
+Migrate the ``setup.py``
+------------------------
+
+The arguments to ``setup()`` in the ``setup.py`` can no longer be automatically generated with ``catkin_pkg``.
+You must pass these arguments manually, which means there will be some duplication with your ``package.xml``.
+
+Start by deleting the import from ``catkin_pkg``.
+
+.. code-block::
+
+ # Delete this
+ from catkin_pkg.python_setup import generate_distutils_setup
+
+Move all arguments given to ``generate_distutils_setup()`` to the call to ``setup()``, and then add the ``install_requires`` and ``zip_safe`` arguments.
+Your call to ``setup()`` should look like this:
+
+.. code-block:: Python
+
+ setup(
+ packages=['talker_py'],
+ package_dir={'': 'src'},
+ install_requires=['setuptools'],
+ zip_safe=True,
+ )
+
+Delete the call to ``generate_distutils_setup()``.
+
+.. code-block::
+
+ # Delete this
+ setup_args = generate_distutils_setup(
+ packages=['talker_py'],
+ package_dir={'': 'src'}
+ )
+
+The call to ``setup()`` needs some `additional metadata `__ copied from the ``package.xml``:
+
+* package name via the ``name`` argument
+* package version via the ``version`` argument
+* maintainer via the ``maintainer`` and ``maintainer_email`` arguments
+* description via the ``description`` argument
+* license via the ``license`` argument
+
+The package name will be used multiple times.
+Create a variable called ``package_name`` above the call to ``setup()``.
+
+.. code-block:: Python
+
+ package_name = 'talker_py'
+
+Copy all of the remaining information into the arguments of ``setup()`` in ``setup.py``.
+Your call to ``setup()`` should look like this:
+
+.. code-block:: Python
+
+ setup(
+ name=package_name,
+ version='1.0.0',
+ install_requires=['setuptools'],
+ zip_safe=True,
+ packages=['talker_py'],
+ package_dir={'': 'src'},
+ maintainer='Brian Gerkey',
+ maintainer_email='gerkey@example.com',
+ description='The talker_py package',
+ license='BSD',
+ )
+
+
+ROS 2 packages must install two data files:
+
+* a ``package.xml``
+* a package marker file
+
+Your package already has a ``package.xml``.
+It describes your package's dependencies.
+A package marker file tells tools like ``ros2 run`` where to find your package.
+
+Create a directory next to the ``package.xml`` called ``resource``.
+Create an empty file in the ``resource`` directory with the same name as the package.
+
+.. tabs::
+
+ .. group-tab:: Linux
+
+ .. code-block:: bash
+
+ mkdir resource
+ touch resource/talker_py
+
+ .. group-tab:: macOS
+
+ .. code-block:: bash
+
+ mkdir resource
+ touch resource/talker_py
+
+ .. group-tab:: Windows
+
+ .. code-block:: bash
+
+ md resource
+ type nul > resource\talker_py
+
+The ``setup()`` call in ``setup.py`` must tell ``setuptools`` how to install these files.
+Add the following ``data_files`` argument to the call to ``setup()``.
+
+.. code-block:: Python
+
+ data_files=[
+ ('share/ament_index/resource_index/packages',
+ ['resource/' + package_name]),
+ ('share/' + package_name, ['package.xml']),
+ ],
+
+Your ``setup.py`` is almost complete.
+
+Migrate Python scripts and create ``setup.cfg``
+-----------------------------------------------
+
+ROS 2 Python packages uses ``console_scripts`` `entry points `__ to install Python scripts as executables.
+The `configuration file `__ ``setup.cfg`` tells ``setuptools`` to install those executables in a package specific directory so that tools like ``ros2 run`` can find them.
+Create a ``setup.cfg`` file next to the ``package.xml``.
+
+.. tabs::
+
+ .. group-tab:: Linux
+
+ .. code-block:: bash
+
+ touch setup.cfg
+
+ .. group-tab:: macOS
+
+ .. code-block:: bash
+
+ touch setup.cfg
+
+ .. group-tab:: Windows
+
+ .. code-block:: bash
+
+ type nul > touch setup.cfg
+
+Put the following content into it:
+
+.. code-block::
+
+ [develop]
+ script_dir=$base/lib/talker_py
+ [install]
+ install_scripts=$base/lib/talker_py
+
+You'll need to use the ``console_scripts`` entry point to define the executables to be installed.
+Each entry has the format ``executable_name = some.module:function``.
+The first part specifies the name of the executable to create.
+The second part specifies the function that should be run when the executable starts.
+This package needs to create an executable called ``talker_py_node``, and the executable needs to call the function ``main`` in the ``talker_py`` module.
+Add the following entry point specification as another argument to ``setup()`` in your ``setup.py``.
+
+.. code-block:: Python
+
+ entry_points={
+ 'console_scripts': [
+ 'talker_py_node = talker_py:main',
+ ],
+ },
+
+The ``talker_py_node`` file is no longer necessary.
+Delete the file ``talker_py_node`` and delete the ``scripts/`` directory.
+
+.. tabs::
+
+ .. group-tab:: Linux
+
+ .. code-block:: bash
+
+ rm scripts/talker_py_node
+ rmdir scripts
+
+ .. group-tab:: macOS
+
+ .. code-block:: bash
+
+ rm scripts/talker_py_node
+ rmdir scripts
+
+ .. group-tab:: Windows
+
+ .. code-block:: bash
+
+ del scripts/talker_py_node
+ rd scripts
+
+The addition of ``console_scripts`` is the last change to your ``setup.py``.
+Your final ``setup.py`` should look like this:
+
+.. code-block:: Python
+
+ from setuptools import setup
+
+ package_name = 'talker_py'
+
+ setup(
+ name=package_name,
+ version='1.0.0',
+ packages=['talker_py'],
+ package_dir={'': 'src'},
+ install_requires=['setuptools'],
+ zip_safe=True,
+ data_files=[
+ ('share/ament_index/resource_index/packages',
+ ['resource/' + package_name]),
+ ('share/' + package_name, ['package.xml']),
+ ],
+ maintainer='Brian Gerkey',
+ maintainer_email='gerkey@example.com',
+ description='The talker_py package',
+ license='BSD',
+ entry_points={
+ 'console_scripts': [
+ 'talker_py_node = talker_py:main',
+ ],
+ },
+ )
+
+Migrate Python code in ``src/talker_py/__init__.py``
+----------------------------------------------------
+
+ROS 2 changed a lot of the best practices for Python code.
+Start by migrating the code as-is.
+It will be easier to refactor code later after you have something working.
+
+Use ``rclpy`` instead of ``rospy``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ROS 2 packages use `rclpy `__ instead of ``rospy``.
+You must do two things to use ``rclpy``:
+
+ 1. Import ``rclpy``
+ 2. Initialize ``rclpy``
+
+Remove the statement that imports ``rospy``.
+
+.. code-block:: Python
+
+ # Remove this
+ import rospy
+
+Rplace it with a statement that imports ``rclpy``.
+
+.. code-block:: Python
+
+ import rclpy
+
+Add a call to ``rclpy.init()`` as the very first statement in the ``main()`` function.
+
+.. code-block:: Python
+
+ def main():
+ # Add this line
+ rclpy.init()
+
+Execute callbacks in the background
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Both ROS 1 and ROS 2 use `callbacks `__.
+In ROS 1, callbacks are always executed in background threads, and users are free to block the main thread with calls like ``rate.sleep()``.
+In ROS 2, ``rclpy`` uses :doc:`Executors <../../Concepts/Intermediate/About-Executors>` to give users more control over where callbacks are called.
+When porting code that uses blocking calls like ``rate.sleep()``, you must make sure that those calls won't interfere with the executor.
+One way to do this is to create a dedicated thread for the executor.
+
+First, add these two import statements.
+
+.. code-block:: Python
+
+ import threading
+
+ from rclpy.executors import ExternalShutdownException
+
+Next, add top-level function called ``spin_in_background()``.
+This function asks the default executor to execute callbacks until something shuts it down.
+
+.. code-block:: Python
+
+ def spin_in_background():
+ executor = rclpy.get_global_executor()
+ try:
+ executor.spin()
+ except ExternalShutdownException:
+ pass
+
+Add the following code in the ``main()`` function just after the call to ``rclpy.init()`` to start a thread that calls ``spin_in_background()``.
+
+.. code-block:: Python
+
+ # In rospy callbacks are always called in background threads.
+ # Spin the executor in another thread for similar behavior in ROS 2.
+ t = threading.Thread(target=spin_in_background)
+ t.start()
+
+
+Finally, join the thread when the program ends by putting this statement at the bottom of the ``main()`` function.
+
+.. code-block:: Python
+
+ t.join()
+
+
+Create a node
+~~~~~~~~~~~~~
+
+In ROS 1, Python scripts can only create a single node per process, and the API ``init_node()`` creates it.
+In ROS 2, a single Python script may create multiple nodes, and the API to create a node is named ``create_node``.
+
+Remove the call to ``rospy.init_node()``:
+
+.. code-block::
+
+ rospy.init_node('talker')
+
+Add a new call to ``rclpy.create_node()`` and store the result in a variable named ``node``:
+
+.. code-block:: Python
+
+ node = rclpy.create_node('talker')
+
+We must tell the executor about this node.
+Add the following line just below the creation of the node:
+
+.. code-block:: Python
+
+ rclpy.get_global_executor().add_node(node)
+
+Create a publisher
+~~~~~~~~~~~~~~~~~~
+
+In ROS 1, users create publishers by instantiating the ``Publisher`` class.
+In ROS 2, users create publishers through a node's ``create_publisher()`` API.
+The ``create_publisher()`` API has an unfortunate difference with ROS 1: the topic name and topic type arguments are swapped.
+
+Remove the creation of the ``rospy.Publisher`` instance.
+
+.. code-block::
+
+ pub = rospy.Publisher('chatter', String, queue_size=10)
+
+Replace it with a call to ``node.create_publisher()``.
+
+.. code-block:: Python
+
+ pub = node.create_publisher(String, 'chatter', 10)
+
+
+Create a rate
+~~~~~~~~~~~~~
+
+In ROS 1, users create ``Rate`` instances directly, while in ROS 2 users create them through a node's ``create_rate()`` API.
+
+Remove the creation of the ``rospy.Rate`` instance.
+
+.. code-block::
+
+ rate = rospy.Rate(10) # 10hz
+
+Replace it with a call to ``node.create_rate()``.
+
+.. code-block:: Python
+
+ rate = node.create_rate(10) # 10hz
+
+Loop on ``rclpy.ok()``
+~~~~~~~~~~~~~~~~~~~~~~
+
+In ROS 1, the ``rospy.is_shutdown()`` API indicates if the process has been asked to shutdown.
+In ROS 2, the ``rclpy.ok()`` API does this.
+
+Remove the statement ``not rospy.is_shutdown()``
+
+.. code-block::
+
+ while not rospy.is_shutdown():
+
+Replace it with a call to ``rclpy.ok()``.
+
+.. code-block:: Python
+
+ while rclpy.ok():
+
+
+Create a ``String`` message with the current time
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You must make a few changes to this line
+
+.. code-block::
+
+ hello_str = "hello world %s" % rospy.get_time()
+
+In ROS 2 you:
+
+* Must get the time from a ``Clock`` instance
+* Should format the ``str`` data using `f-strings `__ since `% is discouraged in active Python versions `__
+* Must instantiate a ``std_msgs.msg.String`` instance
+
+Start with getting the time.
+ROS 2 nodes have a ``Clock`` instance.
+Replace the call to ``rospy.get_time()`` with ``node.get_clock().now()`` to get the current time from the node's clock.
+
+Next, replace the use of ``%`` with an f-string: ``f'hello world {node.get_clock().now()}'``.
+
+Finally, instantiate a ``std_msgs.msg.String()`` instance and assign the above to the ``data`` attribute of that instance.
+Your final code should look like this:
+
+.. code-block:: Python
+
+ hello_str = String()
+ hello_str.data = f'hello world {node.get_clock().now()}'
+
+Log an informational message
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In ROS 2, you must send log messages through a ``Logger`` instance, and the node has one.
+
+Remove the call to ``rospy.loginfo()``.
+
+.. code-block::
+
+ rospy.loginfo(hello_str)
+
+Replace it with a call to ``info()`` on the node's ``Logger`` instance.
+
+.. code-block:: Python
+
+ node.get_logger().info(hello_str.data)
+
+This is the last change to ``src/talker_py/__init__.py``.
+Your file should look like the following:
+
+.. code-block:: Python
+
+ import threading
+
+ import rclpy
+ from rclpy.executors import ExternalShutdownException
+ from std_msgs.msg import String
+
+
+ def spin_in_background():
+ executor = rclpy.get_global_executor()
+ try:
+ executor.spin()
+ except ExternalShutdownException:
+ pass
+
+
+ def main():
+ rclpy.init()
+ # In rospy callbacks are always called in background threads.
+ # Spin the executor in another thread for similar behavior in ROS 2.
+ t = threading.Thread(target=spin_in_background)
+ t.start()
+
+ node = rclpy.create_node('talker')
+ rclpy.get_global_executor().add_node(node)
+ pub = node.create_publisher(String, 'chatter', 10)
+ rate = node.create_rate(10) # 10hz
+
+ while rclpy.ok():
+ hello_str = String()
+ hello_str.data = f'hello world {node.get_clock().now()}'
+ node.get_logger().info(hello_str.data)
+ pub.publish(hello_str)
+ rate.sleep()
+
+ t.join()
+
+
+Build and run ``talker_py_node``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create three terminals:
+
+1. One to build ``talker_py``
+2. One to run ``talker_py_node``
+3. One to echo the message published by ``talker_py_node``
+
+Build the workspace in the first terminal.
+
+.. tabs::
+
+ .. group-tab:: Linux
+
+ .. code-block:: bash
+
+ cd ~/ros2_talker_py
+ . /opt/ros/{DISTRO}/setup.bash
+ colcon build
+
+ .. group-tab:: macOS
+
+ .. code-block:: bash
+
+ cd ~/ros2_talker_py
+ . /opt/ros/{DISTRO}/setup.bash
+ colcon build
+
+ .. group-tab:: Windows
+
+ .. code-block:: bash
+
+ cd \ros2_talker_py
+ call C:\dev\ros2\local_setup.bat
+ colcon build
+
+Source your workspace in the second terminal, and run the ``talker_py_node``.
+
+.. tabs::
+
+ .. group-tab:: Linux
+
+ .. code-block:: bash
+
+ cd ~/ros2_talker_py
+ . install/setup.bash
+ ros2 run talker_py talker_py_node
+
+ .. group-tab:: macOS
+
+ .. code-block:: bash
+
+ cd ~/ros2_talker_py
+ . install/setup.bash
+ ros2 run talker_py talker_py_node
+
+ .. group-tab:: Windows
+
+ .. code-block:: bash
+
+ cd \ros2_talker_py
+ call install\setup.bat
+ ros2 run talker_py talker_py_node
+
+Echo the message published by the node in the third terminal:
+
+.. tabs::
+
+ .. group-tab:: Linux
+
+ .. code-block:: bash
+
+ . /opt/ros/{DISTRO}/setup.bash
+ ros2 topic echo /chatter
+
+ .. group-tab:: macOS
+
+ .. code-block:: bash
+
+ . /opt/ros/{DISTRO}/setup.bash
+ ros2 topic echo /chatter
+
+ .. group-tab:: Windows
+
+ .. code-block:: bash
+
+ call C:\dev\ros2\local_setup.bat
+ ros2 topic echo /chatter
+
+
+You should see messages with the current time being published in the second terminal, and those same messages received in the third.
+
+Refactor code to use ROS 2 convensions
+--------------------------------------
+
+You have successfully migrated a ROS 1 Python package to ROS 2!
+Now that you have something working, consider refactoring it to align better with ROS 2's Python APIs.
+Follow these two principles.
+
+* Create a class that inherits from ``Node``.
+* Do all work in callbacks, and never block those callbacks.
+
+For example, create a ``Talker`` class that inherits from ``Node``.
+As for doing work in callbacks, use a ``Timer`` with a callback instead of ``rate.sleep()``.
+Make the timer callback publish the message and return.
+Make ``main()`` create a ``Talker`` instance rather than using ``rclpy.create_node()``, and give the executor the main thread to execute in.
+
+Your refactored code might look like this:
+
+.. code-block:: Python
+
+ import rclpy
+ from rclpy.node import Node
+ from rclpy.executors import ExternalShutdownException
+ from std_msgs.msg import String
+
+
+ class Talker(Node):
+
+ def __init__(self, **kwargs):
+ super().__init__('talker', **kwargs)
+
+ self._pub = self.create_publisher(String, 'chatter', 10)
+ self._timer = self.create_timer(1 / 10, self.do_publish)
+
+ def do_publish(self):
+ hello_str = String()
+ hello_str.data = f'hello world {self.get_clock().now()}'
+ self.get_logger().info(hello_str.data)
+ self._pub.publish(hello_str)
+
+
+ def main():
+ rclpy.init()
+ try:
+ rclpy.spin(Talker())
+ except (ExternalShutdownException, KeyboardInterrupt):
+ pass
+ finally:
+ rclpy.try_shutdown()
+
+Conclusion
+----------
+
+You have learned how to migrate an example Python ROS 1 package to ROS 2.
+From now on, refer to the :doc:`Migrating Python Packages reference page <./Migrating-Python-Packages>` as you migrate your own Python packages.
diff --git a/source/How-To-Guides/Migrating-from-ROS1/Migrating-Python-Packages.rst b/source/How-To-Guides/Migrating-from-ROS1/Migrating-Python-Packages.rst
index c280e6b845..8e2d20780b 100644
--- a/source/How-To-Guides/Migrating-from-ROS1/Migrating-Python-Packages.rst
+++ b/source/How-To-Guides/Migrating-from-ROS1/Migrating-Python-Packages.rst
@@ -3,8 +3,11 @@
Migration-Guide-Python
The-ROS2-Project/Contributing/Migration-Guide-Python
-Migrating Python Packages
-=========================
+Migrating Python Packages Reference
+===================================
+
+This page is a reference on how to migrate Python packages from ROS 1 to ROS 2.
+If this is your first time migrating a Python package, then follow :doc:`this guide to migrate an example Python package <./Migrating-Python-Package-Example>` first.
.. contents:: Table of Contents
:depth: 2