Python Style Guide¶
Welcome to the Python style guide for MIL. This guide is meant to be a relaxed, easy-to-follow guide with some tips on how to make your Python look snazzy! It’s based on the ROS style guide and the Google Python style guide.
Don’t feel pressured to know or understand every rule, and totally feel free to skim over the guide at first. This guide can provide some helpful information before you begin to write your Python, but it also serves as an excellent place to determine how to use a particular Python feature in your code.
Additionally, this guide is not meant to be permanent! If you have a suggestion, feel free to bring it up and change the guide! Have fun!
The Power to Change Things¶
Before we dive into the Python, a brief word about our code and your ability to change things.
These guidelines are not a hard rule. These guidelines are not permanent, nor should they be. These guidelines were not created by anyone smarter than you.
Therefore, you always have the power to change them. Likewise, if you see code that breaks these guidelines, feel free to change it. If you have suggestions for the guidelines, you should suggest them.
Your innovation and willingness to break and bend the rules that currently exists is what can keep our code powerful, clean, and beautiful.
Features¶
This section explains how to use several of Python’s features to your advantage.
Naming¶
Naming describes how variables, methods, classes, etc. are named.
General¶
Here is a brief table explaining the general pattern of naming:
Type | Public | Internal |
---|---|---|
Packages | lower_with_under |
|
Modules | lower_with_under |
_lower_with_under |
Classes | CapWords |
_CapWords |
Exceptions | CapWords |
|
Functions | lower_with_under() |
_lower_with_under() |
Global/Class Constants | CAPS_WITH_UNDER |
_CAPS_WITH_UNDER |
Global/Class Variables | lower_with_under |
_lower_with_under |
Instance Variables | lower_with_under |
_lower_with_under (protected) |
Method Names | lower_with_under() |
_lower_with_under() (protected) |
Function/Method Parameters | lower_with_under |
|
Local Variables | lower_with_under |
Names to Avoid¶
There are some names we want to avoid! These may be names that are claimed by Python, names that aren’t helpful to other readers of your code, or names that are confusing.
Do not use:
Single character names, except:
In iterators (ex,
for i in range(100):
)e
as an exception handlerf
as a file object
__double_underscore_names__
, because these are reserved by Python!Names that needlessly include a type
Names that are offensive
Names that are meaningless (such as
cool_constant
orfun_variable
)
Mathematical Names¶
Sometimes, we will use our code to implement common mathematics or algorithms. In this case, we may want to use short variable names. Here are some things to consider about that:
Short mathematical names should be generally understood. For example, using
pi
to represent Pi andv
to represent velocity is generally understood and acceptable. However, usingvd
to represent velocity x distance would not be acceptable, as this is not a clear, accepted term.If possible and it makes sense, try using a more descriptive name.
Add a short line comment after the first use of the variable if it could help future readers. You may also desire to include units here as well.
File Naming¶
Files should end with .py
and should not use dashes (-
), but rather underscores (_
). If you do not want the .py
ending on the Python file and would prefer the file to take the role of an executable, consider making a symbolic link or a shell script wrapper that runs the Python file. (This can be as short as exec "$0.py" "$@"
!)
Imports¶
Imports are a powerful feature of Python. Here is a quick guide for using imports in Python:
# Entire modules use
# import x
import datetime
# Specific modules from parent modules use
# from x import y
from dateutil import parser
# Long module names should also use "as" keyword
from example.package import super_long_subpackage_name as super_long
# Common packages can also use "as"
import numpy as np
# For relative imports use .
# For example:
# Assume this file is in /folder/subfolder where mod2 also lives
# Bad:
import mod2
# Good
from folder.subfolder import mod2
Exceptions and assert
¶
Exceptions are a handy feature of Python that helps mitigate code that is breaking. assert
is a helpful keyword in Python that allows one to test whether an internal statement is true, and is often used to test for internal correctness!
When attempting to catch exceptions:
Do not catch
Exception
. Attempting to catchException
will catch every exception thrown, which could indirectly catch exceptions that the program was not meant to catch. Instead, catch for a specific exception that will be raised. There is one case in which catchingException
is acceptable, however:Attempting to catch all exceptions in attempt to mitigate exceptions blocking some code from running. For example, in order to handle exceptions quietly rather than ending a thread which runs a specific process.
Keep
try
blocks to a minimum. Astry
blocks grow in size, the probability of some exception being raised increases, which may hide the true reasons behind some exceptions. Instead, attempt to keep logic outside oftry
blocks and instead only usetry
to catch code that could directly throw the exception you are trying to catch.Feel free to generate your own exception classes which inherit from built-in exception types. However, in many cases, it makes more sense to only use standard exceptions. For example, there is no need to create a new exception class for catching improper types passed into a method - instead, use
TypeError
.
When using assert
:
Keep its use internal. For example, don’t use
assert
to validate human input into a method. Instead, use it to verify to check types or the output of a helper method.
Iterators¶
Iterators provide a powerful way to loop through your code!
When using list comprehensions, each part of the comprehension (
A for B in C
) should fit on one line. If the list comprehension has more than onefor
, then use a traditional iterator.Attempt to use default iterators when possible:
# Yay!
for key in dict:
...
for k, v in dict.items():
...
# Noooo :(
for key in dict.keys():
...
for k, v in dict.iteritems():
...
Yielding¶
Sometimes, it may be desired to have your method yield values rather than return them. Yielding in Python is a powerful feature which delays your method’s execution until you need it.
To have a method yield rather than return, use the yield
keyword in the method and mark the method docstring with Yields:
.
Lambda Functions¶
Lambda functions are mini-functions. They are expressed like so: lambda x, y: x + y
.
If you use lambda functions:
Keep them to one line. If they are longer than 80 characters, use a nested function.
Use them sparingly. Using complex, important operations in a lambda functions makes the code harder to debug and harder for other members to understand.
Conditional Expressions¶
Conditional expressions are short expressions which use the if
and else
keywords. Conditional expressions are powerful tools which can be understood by new editors of the code.
Remember to keep the result, if
, and else
sections of the expression to only one line. For example:
# Good
a = (b if c else d)
really_long = (evaluating_function(parameter)
if really_long_variable_name
else other_really_long_var_name
)
# Bad
super_long = (evaluating_function(parameter)
if (really_long_variable_name_one
and this_iz_cool_func(cats))
else 0
)
Properties¶
Properties are a powerful feature of Python which allow traditional class methods to be hidden as attributes. Sneaky! This adds the benefit of masking getters and setters as traditional properties, but can accidentally mask complexion in the code that should not be hidden.
Properties should:
Not be used for very short operations (such as returning a value)
Not be used when the method invokes a lot of operations. The calling user may not understand this and accidentally invoke a lot of operations that block other processes.
Always be marked with the
@property
decorator.
Implicit True/False¶
Python can evaluate a wide variety of statements to True
and False
. For example:
a = []
if not a:
# If a has no elements
do_something()
b = ""
if b:
# If len(b) > 1
do_something()
In those statements, True
and False
were never explicitly used. However, they were implicitly used.
Attempt to use these statements when possible, as they help to make our code look more crafted and cute. However, keep some things in mind:
When using this type of statement to check the size of a Numpy array, use
if (not) array.size
instead ofif array
.
Decorators¶
Decorator = Dangerous! Sorta. Decorators are powerful for changing the behavior of methods, which can be helpful when operations in the method itself do not suffice.
However, decorators are confusing for new readers of the code, new users to Python, hard to recover from in the case of a raised error, and hard to debug. In the case that a decorator breaks a wrapped function, a MIL member may assume that the function which was operated on by the decorator was at fault, when this may not always be the case.
Therefore, when using decorators, keep in mind:
Please test decorators extensively.
Every decorator should be extensively documented.
Please use decorators judiciously.
Line Length¶
Please keep lines to 80 characters or less. This can be seen in vim by using the option colorcolumn=80
. If you have long strings, then use parentheses to implicitly connect multiple strings together:
dr_seuss = ("Say! I like green eggs and ham!"
"I like them! I do, Sam-I-Am!")
There are some exceptions:
URLs in comments. Don’t split these up.
Blank Line Separators¶
Please do not use backslashes to shorten lines. There is generally no need for them, and they are confusing and hard to format.
Use blank lines to separate different parts of your module. Use two lines to separate top-level elements of any file (whether they are classes or methods) and one line to separate other distinct elements of the file.
Whitespace¶
Whitespace (not to be confused with greenspace, redspace, or rainbowspace!) can be incorrectly used in files to give the appearance of weird formatting.
When using whitespace:
Do not use whitespace in front of a comma or colon.
Always surround comparison operators (
==
,!=
,<
) with a whitespace character.Do not put a whitespace character before an index slice (such as
x [1]
) or function call (such asfun (20)
).Do not include whitespace inside brackets including parentheses, brackets, or braces. For example, use
(x + 1)
, not( x + 1 )
or{'schwartz': True}
, not{ 'schwartz': True }
.In function calls, do not use whitespace when passing a default parameter, unless a type annotation is present. For example, do not use
test(a, b: float=0.0)
, instead usetest(a, b: float = 0.0)
.
String Formatting¶
Many times you will want to format strings in a certain way: to add variables into the string, to make the string look pretty, or to use the string in a certain context, such as an enumerated iterator (for i, v in enumerate(list): print(i + v)
).
In general: use f-strings! f-strings are a unique type of string and can be made by prepending the first single/double quote of a string with f
. This string allows you to add in expressions into the string by using a pair of curly braces:
ultimate_answer = 42
print(f"The ultimate answer is {ultimate_answer}.")
print(f"The ultimate answer is not {ultimate_answer // 2}!")
When formatting a string that will be printed for logging/debugging, attempt to use a common starting term such that the string can be found with a search tool, such as grep
or the terminal’s search functionality. Additionally, make clear what sections of the logged method are interpolated or calculated.
# Please no!
import random
rand = random.random()
print(f"The {rand} number is less than 1.")
# Now, you can search for "This number is less than one" and find all matching instances!
print(f"This number is less than one: {rand}")
TODO Comments¶
TODO comments are a great way to mark a piece of code as not currently finished. To use TODO comments, create a comment that starts with TODO
, has your name, and what still needs to be done.
# TODO (Dr. Schwartz) - Finish method to add c in final sum
def sum(a: int, b: int, c: int):
return a + b
Getters and Setters¶
Getters and setters serve as dedicated methods for getting and setting a property of a class. These are similar to @property
methods, but these are explicit methods which are not hidden as properties.
Getters and setters should only be used when changing a single property causes significant overhead or recalculation of other properties. Otherwise, set the property as public or use a small @property
method.
Getters and setters should be named in a way that demonstrates which property is being set. For example, get_weather()
and set_weather()
.
Function Length¶
A function should roughly be 30 lines. This is not a hard rule, but is a general rule to keep in mind when writing and reading functions. If you see a function longer than this, feel free to break it up if it makes sense.
Typing¶
Please, whenever possible, add type annotations to your code. Typing is a powerful feature of Python that helps others read and understand your code.
Type annotations take on the following format:
def sum(a: int, b: int) -> int:
return a + b
The int
keyword after the a
and b
parameters of the method signals that the method accepts two integers. The -> int
at the end of the function signature signals that the method then returns an int
.
Other common built-in type annotations you might see include float
, dict
, bool
, or None
. Sometimes, though type annotations can be a little more complex. How do you write the type of a parameter that is optional? Or how you do you write the type annotation of a method that can return a List
or a dict
? Or how about a method that returns another method?? (Method inception!)
This is where the built-in typing
module comes in. This handy module contains all sorts of classes to represent types throughout your code. Let’s have a look.
from typing import List, Union, Optional, Callable
def crazy_func(a: int, b: Union[List[float], dict], c: Optional[dict] = None) -> Callable:
# Do some crazy stuff
return d
What the heck is that method even saying?
First,
a
accepts typeint
.Second,
b
accepts either (this is what theUnion
class represents) a list offloat
s or adict
.c
can accept adict
, but it doesn’t have to - supplying this parameter isOptional
.The function returns another method, known to
typing
as aCallable
.
Note that if your type annotations are overly long, it may be a good idea to split up your method into a series of shorter methods.
Docstrings¶
Docstrings stand for doctor strings, strings that can only be created by medical professionals. Kidding! Docstrings stand for strings that document particular aspects of one’s code. Docstrings can be used to document modules, methods, and classes!
General Docstrings¶
For all docstrings, use narrative text. It doesn’t have to be your best literary work, but please make descriptions in your docstrings readable.
Modules¶
Each module should have a docstring placed in its __init__.py
file, at the top of the file. This docstring should explain what the module does. First, a one-line sentence should be added to explain briefly what the module does. Then, below this sentence, evaluate more on what the module does. Then, below the longer description, add one or two brief examples of the module’s use case.
# calculator/__init__.py
"""
Provides several calculation methods to add, subtract, multiply, and divide
integers.
Each calculation method is structured into a separate method. Some methods can
also accept any amount of arguments, while others only accept two arguments.
Some methods also support different methods of calculation or provide
optimizations to speed up performance.
Example 1:
calc = Calculator()
calc.add(4, 3) # 7
calc.subtract_many(4, 3, 5) # 4 - 3 - 5 = -4
"""
Classes¶
Every class should also have a docstring. Begin the docstring by briefly elaborating on the purpose of the class before explaining more below, in a similar fashion to the module docstring. Then, add any public attributes provided by the class that are useful to the caller.
# calculator/history.py
class CalculatorHistory:
"""
Stores the history of completed operations on the calculator.
Beyond storing the operations completed, the class also provides methods
for completing statistics on the operations completed. Individual entries
can be viewed or deleted from the history.
Attributes:
history_length (int): The number of entries in the current history.
oldest_entry (datetime.datetime): The time that the oldest entry in the
history was executed.
"""
def __init__(self):
...
Methods¶
Like modules and classes, methods should also have docstrings! Begin method docstrings with a brief explanation, followed by a longer, more detailed explanation if needed. Then, add the following three sections of the docstring:
Args:
- The arguments accepted by the function. List each argument name, its type annotation, and what it is in English. If multiple positional arguments (such as*args
) or multiple keyword arguments (such as**kwargs
) are accepted by the method, write these in the docstring as well.Returns:
- The type annotation of the method’s returned value, as well as an explanation of what is returned in English.Raises:
- Any exceptions that are raised by the method. List the type of exception raised and what condition will cause the exception to be raised (in English).
# calculator/history.py
class CalculatorHistory:
...
def retrieve_entries(self,
history_file: str,
*entries_indexes: int,
/,
return_only_since: Optional[datetime.datetime] = None
) -> List[HistoryEntry]:
"""
Retrieves a series of entries in the calculator history.
Used to obtain a set of individual entries. For a general range of
entries, use retrieve_range() instead.
Args:
history_file (str): The name of the history file to retrieve the
entries from.
*entries_indexes (int): A list of indexes to retrieve entries at.
return_only_since (Optional[datetime.datetime]): If specified,
then only return entries from a particular point. Keyword-only parameter.
Returns:
List[HistoryEntry]: A list of the history entries that match
the specified indices.
Raises:
IndexError: The desired index does not exist in the history
record.
"""
Linting & CI¶
We are currently in the process of setting up linting and CI for our Python systems.
Other Tools¶
Other tools that can make working with Python easier in our codebase include code completion and checking systems. You can install these in popular editors such as VSCode or Vim.