Documentation#

As we have probably all heard before, good documentation is almost as important (if not equally as important) as good code itself. You may have written some elegant and powerful code to solve your problems today, but weeks or months from now, that code may become functionally useless if you forget what it does or how to call it. Python3 users have a special built-in tool at their disposal called docstrings that make documenting functions easy. After going through this module, students should be able to:

  • Write well-crafted docstrings for all functions

  • Add type hints to function definitions

Docstrings#

Docstrings are special strings that appear immediately following function definitions in our code. They should be surrounded by three double-quotation marks on each side, and they may span multiple lines. For example:

def a_function():
    """
    This is a docstring.
    """
    # code goes here
    return

The above is a valid docstring, but it is not a very helpful docstring. When you write docstrings, at a minimum try to include the following sections:

  1. A short description of the purpose of the function

  2. A list of arguments, including type

  3. A list of returned values, including type

A better template for a docstring (based on the Google Style Guide) might look like:

def a_function(arg1, arg2):
    """
    This function does XYZ.

    Args:
        arg1 (type): Define what is expected for arg1.
        arg2 (type): Define what is expected for arg2.

    Returns:
        result (type): Define what is expected for result.
    """
    # code goes here
    return(result)

The description should be succinct, yet complete. Arguments should be listed by name and the expected type (e.g., bool, float, str, etc) should be stated. And the return result(s) should be listed along with the expected type(s).

Let’s look at one more example using a real function:

def add_and_square(num1, num2):
    """
    Given two numbers, this function will first add them together, then square the sum
    and return the result.

    Args:
        num1 (float): The first number.
        num2 (float): The second number.

    Returns:
        result (float): The square of the sum of input arguments.
    """
    result = (num1+num2)**2
    return(result)

Note

Notice above we are using more-or-less complete sentences with proper grammar.

Next, let’s add docstrings to our groceries.py code we have been working on:

 1#!/usr/bin/env python3
 2import json
 3
 4def compute_average_quantity(a_list_of_dicts, a_key_string):
 5    """
 6    Iterate through a list of dictionaries, pulling out values associated with
 7    a given key. Returns the average of those values.
 8
 9    Args:
10        a_list_of_dicts (list): A list of dictionaries, each dict should have the
11                                same set of keys.
12        a_key_string (string): A key that appears in each dictionary associated
13                            with the desired value( will enforce float type).
14
15    Returns:
16        result (float): Average value.
17    """
18
19    total_quantity = 0
20    for item in a_list_of_dicts:
21        total_quantity += item[ a_key_string ]
22
23    return float( total_quantity / len( a_list_of_dicts ) )
24
25def calc_total_price( price, quantity ):
26    """
27    ???
28    """
29
30    total_price = price * quantity
31    return total_price
32
33def count_categories(a_list_of_dicts, a_key_string):
34    """
35    Given a list of dictionaries and a key, count the number of times each category
36    appears in the list of dictionaries.
37
38    Args:
39        a_list_of_dicts (list): A list of dictionaries, each dict should have the
40                                same set of keys.
41        a_key_string (string): A key that appears in each dictionary associated
42                            with the desired value.
43
44    Returns:
45        categories_observed (dict): A dictionary where the keys are the categories
46                                    observed in the list of dictionaries and the values
47                                    are the number of times that category appears
48    """
49    categories_observed = {}
50    for item in a_list_of_dicts:
51        if item[a_key_string] in categories_observed:
52            categories_observed[item[a_key_string]] += 1
53        else:
54            categories_observed[item[a_key_string]] = 1
55    return(categories_observed)
56
57def main():
58    with open('groceries.json', 'r') as f:
59        grocery_data = json.load( f )
60
61    print( compute_average_quantity( grocery_data['items'], 'quantity' ) )
62
63    for row in grocery_data['items']:
64        total_price = calc_total_price( float( row['price']), float( row['quantity'] ) )
65        print(f'Total Price: {total_price:.2f}')
66
67    print( count_categories( grocery_data['items'], 'category' ) )
68
69if __name__ == '__main__':
70    main()

In general, your main() function usually does not need a docstring. It is good habit to write the main() function simply and clearly enough that it is self explanatory, with perhaps a few comments to help. If you do add a docstring to the main() function, you may write a few short summary sentences but omit the Args and Returns sections.

EXERCISE#

Write the missing docstring for the calc_total_price() function above.

EXERCISE#

Open up the Python3 interactive interpreter. Import your groceries.py methods. Use the commands dir() and help() to find and read the docstrings that you wrote.

Type Hints#

Type hints in function definitions indicate what types are expected as input and output of a function. No checking actually happens at runtime, so if you send the wrong type of data as an argument, the type hint itself won’t cause it to return an error. Think of type hints simply as documentation or annotations to help the reader understand how to use a function.

Warning

In the code blocks below, we omit docstrings for brevity only. Please keep including docstrings in your code.

Type hints should take form:

def a_function(arg_name: arg_type) -> return_type:
    # code goes here
    return(result)

In the above example, we are providing a single argument called arg_name that should be of type arg_type. The expected return value should be return_type. Let’s look at an example using a real function:

def add_and_square(num1: float, num2: float) -> float:
    result = (num1+num2)**2
    return(result)

Next, add type hints to the function definitions of the groceries.py script (only showing snippets below):

def compute_average_quantity(a_list_of_dicts: list[dict], a_key_string: str) -> float:
def calc_total_price(price: float, quantity: float) -> float:
def count_categories(a_list_of_dicts, a_key_string):  # what about this one?

Although Python3 does not check or enforce types at run time, there are other tools that make use of type hints to check types at the time of development. For example, some IDEs (including PyCharm) will evaluate type hints as you write code and provide an alert if you call a function in a way other than what the type hint suggests. In addition, there are Python3 libraries like mypy that can wrap your Python3 programs and check / evaluate type hints as you go, provided errors where types don’t match.

Warning

Be aware that there is some redundancy in the information contained in type hints and in the docstrings. Be careful not to let them get out of sync as your code evolves.

Additional Resources#