Note
This section is optional, the ROS2 tutorial starts at ROS2 Installation.
(Murilo’s) Python Best Practices
Warning
This tutorial expects prior knowledge in Python and object-oriented programming. As such, this section is not meant to be a comprehensive Python tutorial. You have better resources made by smarter people available online, e.g. The Python Tutorial.
Terminology
Let’s go through the Python terminology used in this tutorial. This terminology is not necessarily uniform with other sources/tutorials you might find elsewhere. It is based on my interpretation of The Python Tutorial on Modules, the Python Glossary, and my own experience.
Term |
Book Definition |
Use in the wild |
---|---|---|
script |
A Python file that can be executed. |
Any Python file meant to be executed. |
module |
A file with content that is meant to be imported by other modules and scripts. |
This term is used very loosely and can basically mean any Python file, but usually a Python file meant to be imported from. |
package |
A collection of modules. |
A folder with an |
Use a venv
We already know that it is a good practice to When you want to isolate your environment, use venv. So, let’s turn that into a reflex and do so for this whole section.
cd ~
source ros2tutorial_venv/bin/activate
Minimalist package: something to start with
In this step, we’ll work on these.
python/minimalist_package/
└── minimalist_package/
└── __init__.py
First, let’s make a folder for our project
Hint
The -p
option for mkdir creates all parent folders as well, when they do not exist.
mkdir -p ~/ros2_tutorials_preamble/python/minimalist_package
Then, let’s create a folder with the same name within it for our package. A Python package is a folder that has an __init__.py
, so for now we add an empty __init__.py
by doing so
cd ~/ros2_tutorials_preamble/python/minimalist_package
mkdir minimalist_package
cd minimalist_package
touch __init__.py
The (empty) package is done!
Hint
In PyCharm
, open the ~/ros2_tutorials_preamble/python/minimalist_package
folder to correctly interact with this project.
Warning
It is confusing to have two nested folders with the same name. However, this is quite common and
starts to make sense after getting used to it (it is also the norm in ROS2). The first folder
is supposed to be how your file system sees your package, i.e. the project folder,
and the other contains the actual Python package, with the __init__.py
and other source code.
Minimalist script
In this step, we’ll work on this.
python/minimalist_package/
└── minimalist_package/
└── __init__.py
└── minimalist_script.py
Let’s start with a minimalist script that prints a string periodically,
as follows. Create a file in ~/ros2_tutorials_preamble/python/minimalist_package/minimalist_package
called minimalist_script.py
with the following
contents.
1#!/bin/python3
2import time
3
4
5def main() -> None:
6 """An example main() function that prints 'Howdy!' twice per second."""
7 try:
8 while True:
9 print("Howdy!")
10 time.sleep(0.5)
11 except KeyboardInterrupt:
12 pass
13 except Exception as e:
14 print(e)
15
16
17if __name__ == "__main__":
18 """When this module is run directly, it's __name__ property will be '__main__'."""
19 main()
Running a Python script on the terminal
There are a few ways to run a script/module in the command line. Without worrying about file permissions, specifying that the file must be interpreted by Python (and which version of Python) is the most general way to run a script
cd ~/ros2_tutorials_preamble/python/minimalist_package/minimalist_package
python3 minimalist_script.py
which will output
Hint
You can end the minimalist_script.py by pressing CTRL+C in the terminal in which it is running.
Howdy!
Howdy!
Howdy!
Another way to run a Python script is to execute it directly in the terminal. This can be done with
cd ~/ros2_tutorials_preamble/python/minimalist_package/minimalist_package
./minimalist_script.py
which will result in
bash: ./minimalist_script.py: Permission denied
because our file does not have the permission to run as an executable. To give it that permission, we must run ONCE
cd ~/ros2_tutorials_preamble/python/minimalist_package/minimalist_package
chmod +x minimalist_script.py
and now we can run it properly with
cd ~/ros2_tutorials_preamble/python/minimalist_package/minimalist_package
./minimalist_script.py
resulting in
Howdy!
Howdy!
Howdy!
Note that for this second execution strategy to work, we MUST have the #!, called shebang, at the beginning of the first line. The path after the shebang specifies what program will be used to interpret that file. In general, differently from Windows, Ubuntu does not guess the file type by the extension when running it.
#!/bin/python3
If we remove the shebang line and try to execute the script, it will return the following errors, because Ubuntu doesn’t know what to do with that file.
./minimalist_script.py: line 2: import: command not found
./minimalist_script.py: line 5: syntax error near unexpected token `('
./minimalist_script.py: line 5: `def main() -> None:'
When using if __name__=="__main__":
, just call the real main()
There are multiple ways of running a Python script. In the one we just saw, the name
of the module becomes __main__
, but in others that does not happen, meaning that the if
can be completely skipped.
So, write the main()
function of a script as something standalone and, in the condition, just call it and do nothing else, as shown below
if __name__ == "__main__":
"""When this module is run directly, it's __name__ property will be '__main__'."""
main()
It’s dangerous to go alone: Always wrap the contents of main
function on a try–except block
It is good practice to wrap the contents of main()
call in a try--except
block
with at least the KeyboardInterrupt
clause. This allows the user to shutdown
the module cleanly either through the terminal or through PyCharm. We have done so in the example as follows
def main() -> None:
"""An example main() function that prints 'Howdy!' twice per second."""
try:
while True:
print("Howdy!")
time.sleep(0.5)
except KeyboardInterrupt:
pass
except Exception as e:
print(e)
This is of particular importance when hardware is used, otherwise, the connection with it might be left in an undefined state causing difficult-to-understand problems at best and physical harm at worst.
The Exception
clause in our example is very broad, but a MUST in code that is still under development.
Exceptions of all sorts can be generated when there is a communication error with the hardware, software (internet, etc),
or other issues.
This broad Exception
clause could be replaced for a less broad exception handling if that makes sense in a given
application, but that is usually not necessary nor safe. When handling hardware, it is, in general, IMPOSSIBLE to
test the code of all combinations of inputs and states. As they say,
Be wary, for overconfidence is a slow and insidious [source for terrible bugs and failed demos]
Hint
Catching all Exceptions
might make debugging more difficult in some cases. At your own risk, you can remove
this clause temporarily when trying to debug a stubborn bug, at the risk of forgetting to put it back and ruining
your hardware.
Minimalist class: Use classes profusely
In this step, we’ll work on these.
python/minimalist_package/
└── minimalist_package/
└── __init__.py
└── minimalist_script.py
└── _minimalist_class.py
As you are familiar with object-oriented programming, you know that classes are central to this paradigm. As a memory refresher, let’s make a class that honestly does nothing really useful but illustrates all the basic points in a Python class.
Create a file in ~/ros2_tutorials_preamble/python/minimalist_package/minimalist_package
called _minimalist_class.py
with the following
contents.
1class MinimalistClass:
2 """
3 A minimalist class example with the most used elements.
4 https://docs.python.org/3/tutorial/classes.html
5 """
6 # Attribute reference, accessed with MinimalistClass.attribute_reference
7 attribute_reference: str = "Hello "
8
9 def __init__(self,
10 attribute_arg: float = 10.0,
11 private_attribute_arg: float = 20.0): # With a default value of 20.0
12 """The __init__ works together with __new__ (not shown here) to
13 construct a class. Loosely it is called the Python 'constructor' in
14 some references, although it is officially an 'initializer' hence
15 the name.
16 https://docs.python.org/3/reference/datamodel.html#object.__init__
17 It customizes an instance with input arguments.
18 """
19 # Attribute that can be accessed externally
20 self.attribute: float = attribute_arg
21
22 # Attribute that should not be accessed externally
23 # a name prefixed with an underscore (e.g. _spam) should be treated
24 # as a non-public part of the API (whether it is a function, a method or a data member).
25 # It should be considered an implementation detail and subject to change without notice.
26 self._private_attribute: float = private_attribute_arg
27
28 def method(self) -> float:
29 """Methods with 'self' should use at least one statement in which 'self' is required."""
30 return self.attribute + self._private_attribute
31
32 def set_private_attribute(self, private_attribute_arg: float) -> None:
33 """If a private attribute should be writeable, define a setter."""
34 self._private_attribute = private_attribute_arg
35
36 def get_private_attribute(self) -> float:
37 """If a private attribute should be readable, define a getter."""
38 return self._private_attribute
39
40 @staticmethod
41 def static_method():
42 """
43 Methods that do not use the 'self' should be decorated with the @staticmethod.
44 It will only have access to attribute references.
45 https://docs.python.org/3.10/library/functions.html#staticmethod
46 """
47 return MinimalistClass.attribute_reference + "World!"
then, let’s modify the __init__.py
with the
following contents
1"""
2Having an __init__.py file within a directory turns it into a Python Package.
3A package within a package is called a subpackage.
4https://docs.python.org/3/tutorial/modules.html#packages
5"""
6from minimalist_package._minimalist_class import MinimalistClass
Note
When adding imports to the __init__.py
, the folder that we use to open in Pycharm and that we call to execute
the scripts is extremely relevant. When packages are deployed (e.g. in PyPI or ROS2), the “correct”
way to import in __init__.py
is to use import <PACKAGE_NAME>.<THING_TO_IMPORT>
, which is why we’re doing
it this way.
Note
Relative imports such as .<THING_TO_IMPORT>
might work in some cases, and that is fine. It is a supported
and valid way to import. However, don’t be surprised when it doesn’t work in ROS2, PyPI packages, etc, and generates
a lot of frustration.
Not a matter of taste: Code style
It might be parsing through jibber-jabber code in l__tcode lessons with weird C-pointer logic and nested dereference operators that gets you through the door into one of those fancy companies with no dress code and free snacks, perks that I’m totally not envious of one bit. In the ideal world, at least, writing easy-to-understand code with the proper style is what should keep you in that job.
So, always pay attention to the naming of classes (PascalCase), files and functions (snake_case), etc.
Thankfully, Python has a bunch of style rules builtin the language and PEP, such as PEP8. Take this time to read it and get inspired by The Zen of Python
Beautiful is better than ugly.Explicit is better than implicit.Simple is better than complex.Complex is better than complicated.Flat is better than nested.Sparse is better than dense.Readability counts.Special cases aren’t special enough to break the rules.Although practicality beats purity.Errors should never pass silently.Unless explicitly silenced.In the face of ambiguity, refuse the temptation to guess.There should be one– and preferably only one –obvious way to do it.Although that way may not be obvious at first *unless you’re Dutch*.Now is better than never.Although never is often better than *right now.*If the implementation is hard to explain, it’s a bad idea.If the implementation is easy to explain, it may be a good idea.Namespaces are one honking great idea – let’s do more of those!
Take the (type) hint: Always use type hints
Note
For more info, check out the documentation on Python typing and the type hints cheat sheet
Before you flood my inbox with complaints, let me vent for you. A preemptive vent.
But, you know, one of the cool things in Python is that we don’t have to explicitly type variables. Do you want to turn Python into C?? Why do you love C++ so much you unpythonic Python hater????
The dynamic typing nature of Python is, no doubt, a strong point of the language. Note that adding type hints does not impede your code to be used with other types as arguments. Type hints are, to no one’s surprise, hints to let users (and some automated tools) know what types your functions were made for, e.g. to allow your favorite IDE to help you with code suggestions.
In these tutorials, we are not going to use any complex form of type hints. We’re basically going to attain ourselves to the simplest two forms, the (attribute, argument, etc) type, and the return types.
For attributes we use <attribute>: type
, as shown below
self.attribute: float = attribute_arg
For method arguments we use <argument>: <type>
and for return types we use def <method>(<params>) -> <type>
, as shown below in our example
def set_private_attribute(self, private_attribute_arg: float) -> None:
"""If a private attribute should be writeable, define a setter."""
self._private_attribute = private_attribute_arg
Document your code with Docstrings
You do not need to document every single line you code, that would in fact be quite obnoxious
# c stores the sum of a and b
c = a + b
# d stores the square of c
d = c**2
# check if d is zero
if d == 0:
# Print warning
print("Warning")
But, on the other side of the coin, it doesn’t take too long for us to forget what the parameters of a function mean. Take the (type) hint: Always use type hints helps a lot, but additional information is always welcome. If you get used to using docstrings for every new method, your programming will be better in general because documenting your code makes you think about it.
The example below shows a quick explanation of what the class does using a docstring
class MinimalistClass:
"""
A minimalist class example with the most used elements.
https://docs.python.org/3/tutorial/classes.html
"""
The PEP 257 talks about docstrings but does not define too much beyond saying that we should use it. My recommendation as of now would be the Sphinx markup, because of the many Python libraries using it for Sphinx documentation/tutorials like this one.
The sample code shown in this section has docstrings everywhere, but they are being used to explain the general usage of some Python syntax. When documenting your code, obviously, the documentation should be about what the method/class/attribute does.
Hint
Ideally, all documentation is perfect from the start. In reality, however, that rarely ever happens so some documentation is always better than none. My advice would be to write something as it goes and possibly adjust it to more stable or cleaner documentation when the need arises.
Unit tests: always test your code
Note
For a comprehensive tutorial on unit testing go through the unittest docs.
In this step, we’ll work on these.
python/minimalist_package/
└── minimalist_package/
└── __init__.py
└── minimalist_script.py
└── _minimalist_class.py
└── test/
└── test_minimalist_class.py
Unit testing is a flag that has been waved by programming enthusiasts and is often a good measurement of code maturity.
The elephant in the room is that writing unit tests is boring. Yes, we know, very boring.
Unit tests are boring because they are an investment. Unit testing won’t necessarily make your code […] better, faster, […] right now. However, without tests, don’t be surprised after some point if your implementations make you drown in tech debt. Dedicating a couple of minutes now to make a couple of tests when your codebase is still in its infancy makes it more manageable and less boresome.
Back to the example, a good practice is to create a folder name test
at the same level as the packages to be tested, like so
cd ~/ros2_tutorials_preamble/python/minimalist_package
mkdir test
Then, we create a file named test_minimalist_class.py
with the contents below in the test
folder.
Note
The prefix test_
is important as it is used by some frameworks to automatically discover tests. So it is better not to use
that prefix if that file does not contain a unit test.
1import unittest
2from minimalist_package import MinimalistClass
3
4
5class TestMinimalistClass(unittest.TestCase):
6 """For each `TestCase`, we create a subclass of `unittest.TestCase`."""
7
8 def setUp(self):
9 self.minimalist_instance = MinimalistClass(attribute_arg=15.0,
10 private_attribute_arg=35.0)
11
12 def test_attribute(self):
13 self.assertEqual(self.minimalist_instance.attribute, 15.0)
14
15 def test_private_attribute(self):
16 self.assertEqual(self.minimalist_instance._private_attribute, 35.0)
17
18 def test_method(self):
19 self.assertEqual(self.minimalist_instance.method(), 15.0 + 35.0)
20
21 def test_get_set_private_attribute(self):
22 self.minimalist_instance.set_private_attribute(20.0)
23 self.assertEqual(self.minimalist_instance.get_private_attribute(), 20.0)
24
25 def test_static_method(self):
26 self.assertEqual(MinimalistClass.static_method(), "Hello World!")
27
28
29def main():
30 unittest.main()
Running the tests
For a quick jolt of instant gratification, let’s run the tests before we proceed with the explanation.
There are many ways to run tests written with unittest
. The following will run all tests found in the folder test
cd ~/ros2_tutorials_preamble/python/minimalist_package
python -m unittest discover -v test
which will output
test_attribute (test_minimalist_class.TestMinimalistClass) ... ok
test_get_set_private_attribute (test_minimalist_class.TestMinimalistClass) ... ok
test_method (test_minimalist_class.TestMinimalistClass) ... ok
test_private_attribute (test_minimalist_class.TestMinimalistClass) ... ok
test_static_method (test_minimalist_class.TestMinimalistClass) ... ok
----------------------------------------------------------------------
Ran 5 tests in 0.000s
OK
Yay! We’ve done it!
Start with use unittest
Note
ROS2 uses pytest
as default, but that doesn’t mean you also have to use it in every Python code you ever write.
There are many test frameworks for Python. Nonetheless, the unittest module is built into Python so, unless you have a very good reason not to use it, just [use] it.
We import the unittest
module along with the class that we want to test, namely MinimalistClass
.
import unittest
from minimalist_package import MinimalistClass
Test them all
Note
Good unit tests will not only let you know when something broke but also where it broke. A failed test of a high-level function might not give you too much information, whereas a failed test of a lower-level (more fundamental) function will allow you to pinpoint the issue.
Unit tests are somewhat like insurance. The more coverage you have, the better. In this example, we test all the elements in the class. Each test will be based on one or more asserts. For more info check the unittest docs.
In a few words, we make a subclass of unittest.TestCase
and create methods within it that test one part
of the code, hence the name unit tests.
def test_attribute(self):
self.assertEqual(self.minimalist_instance.attribute, 15.0)
def test_private_attribute(self):
self.assertEqual(self.minimalist_instance._private_attribute, 35.0)
def test_method(self):
self.assertEqual(self.minimalist_instance.method(), 15.0 + 35.0)
def test_get_set_private_attribute(self):
self.minimalist_instance.set_private_attribute(20.0)
self.assertEqual(self.minimalist_instance.get_private_attribute(), 20.0)
def test_static_method(self):
self.assertEqual(MinimalistClass.static_method(), "Hello World!")
If one of the asserts
fails, then the related test will fail, and the test framework will let us know which one.
The test’s main function
Generally, a test script based on unittest will have the following main function. It will run all available tests in our test class. For more info and alternatives check the unittest docs.
def main():
unittest.main()