August 15, 2022

How to Use Python Docstrings for Effective Code Documentation

Documenting in Python with docstrings

Documenting your code is a critical skill for any data scientist or software engineer. Learn how to do it using docstrings.

Why Documentation in Python Is Important?

The Zen of Python tells us that "Readability counts" and "Explicit is better than implicit." These are necessary characteristics of Python. When we write code, we do it for end-users, developers, and ourselves.

Remember that we are also end-users when we read pandas documentation or scikit-learn documentation. These two packages have excellent documentation, and users usually don’t have any trouble using them because they contain a lot of examples and tutorials. They also have built-in documentation that we can directly access in your preferred IDE.

Now imagine using these packages without any reference. We would need to dig into their code to understand what it does and how we can employ it. There are some packages with absolutely zero documentation, and it usually takes much more time to understand what is under their hoods and how we can use them. This workload doubles or triples if the package is huge, with functions spread over multiple files.

It should be more obvious now why good documentation counts.

Next, other developers may want to contribute to our projects. They can do it much faster and more efficiently if our code is well-documented. Would you want to contribute to a project in your free time if you had to spend hours to figure out what its different parts do? I wouldn’t.

Finally, even if it’s only our private project or a small script, we need the documentation anyway. We never know when we will return to one of our old projects to modify or improve it. It’s so pleasant and easy to work with it if it explicitly tells us what was it for and what its functions, code lines, and modules do. We tend to forget quickly what we were thinking during code-writing, so spending some time explaining why we are doing something always saves more time when returning to the code (even if it was written just one day ago). So, invest some time in documentation, and it will reward you later.

Python Docstrings Examples

Let’s go over some examples of docstrings in Python! For example, a banal function below takes two variables and either returns their sum (by default) or the difference between them:

def sum_subtract(a, b, operation="sum"):
    if operation == "sum":
        return a + b
    elif operation == "subtract":
        return a - b
    else:
        print("Incorrect operation.")

print(sum_subtract(1, 2, operation="sum"))
    3

This function is pretty straightforward, but for the sake of demonstrating the power of Python docstrings, let’s write some documentation:

def sum_subtract(a, b, operation="sum"):
    """
    Return sum or difference between the numbers 'a' and 'b'.
    The type of operation is defined by the 'operation' argument.
    If the operation is not supported, print 'Incorrect operation.'
    """
    if operation == "sum":
        return a + b
    elif operation == "subtract":
        return a - b
    else:
        print("Incorrect operation.")

The Python docstring of this function is enclosed between three double quotes from both sides. As you can see, this string explains what this function does and indicates how we can change its functionality — and what happens if it doesn’t support the action we want it to perform. It was a simple example, and you may argue that this function is too obvious to require any explanation, but once functions get complicated and their number increases, you’ll need at least some documentation to avoid getting lost in your own code.

What’s happening under the hood? If we run the help() function on the sum_subtract() function, the docstring pops up. All well-documented packages have docstrings for (almost) all of their functions. For example, let’s see a DataFrame in pandas:

import pandas as pd
help(pd.DataFrame)

# Help on class DataFrame in module pandas.core.frame:

# class DataFrame(pandas.core.generic.NDFrame, pandas.core.arraylike.OpsMixin)
#  |  DataFrame(data=None, index: 'Axes | None' = None, columns: 'Axes | None' = None, dtype: 'Dtype | None' = None, copy: 'bool | None' = None)
#  |
#  |  Two-dimensional, size-mutable, potentially heterogeneous tabular data.
#  |
#  |  Data structure also contains labeled axes (rows and columns).
#  |  Arithmetic operations align on both row and column labels. Can be
#  |  thought of as a dict-like container for Series objects. The primary
#  |  pandas data structure.
#  |
#  |  Parameters
#  |  ----------
#  |  data : ndarray (structured or homogeneous), Iterable, dict, or DataFrame
#  |      Dict can contain Series, arrays, constants, dataclass or list-like objects. If
#  |      data is a dict, column order follows insertion-order. If a dict contains Series
#  |      which have an index defined, it is aligned by its index.
#  |
#  |      .. versionchanged:: 0.25.0
#  |         If data is a list of dicts, column order follows insertion-order.
#  |
#  |  index : Index or array-like
#  |      Index to use for resulting frame. Will default to RangeIndex if
#  |      no indexing information part of input data and no index provided.
#  |  columns : Index or array-like
#  |      Column labels to use for resulting frame when data does not have them,
#  |      defaulting to RangeIndex(0, 1, 2, ..., n). If data contains column labels,
#  |      will perform column selection instead.
#  |  dtype : dtype, default None
#  |      Data type to force. Only a single dtype is allowed. If None, infer.
#  |  copy : bool or None, default None
#  |      Copy data from inputs.
#  |      For dict data, the default of None behaves like ``copy=True``.  For DataFrame
#  |      or 2d ndarray input, the default of None behaves like ``copy=False``.
#  |
#  |      .. versionchanged:: 1.3.0

# Docstring continues...

Now, if we have a look at the pandas source code, we will find out that help shows us the docstring of the DataFrame class (search for class DataFrame(NDFrame, OpsMixin)).

Technically, the docstring is assigned to an automatically generated attribute of this object called __doc__. We can also print out this property and see that it is exactly the same as before:

print(pd.DataFrame.__doc__[:1570])  # Truncated
    Two-dimensional, size-mutable, potentially heterogeneous tabular data.

    Data structure also contains labeled axes (rows and columns).
    Arithmetic operations align on both row and column labels. Can be
    thought of as a dict-like container for Series objects. The primary
    pandas data structure.

    Parameters
    ----------
    data : ndarray (structured or homogeneous), Iterable, dict, or DataFrame
        Dict can contain Series, arrays, constants, dataclass or list-like objects. If
        data is a dict, column order follows insertion-order. If a dict contains Series
        which have an index defined, it is aligned by its index.

        .. versionchanged:: 0.25.0
           If data is a list of dicts, column order follows insertion-order.

    index : Index or array-like
        Index to use for resulting frame. Will default to RangeIndex if
        no indexing information part of input data and no index provided.
    columns : Index or array-like
        Column labels to use for resulting frame when data does not have them,
        defaulting to RangeIndex(0, 1, 2, ..., n). If data contains column labels,
        will perform column selection instead.
    dtype : dtype, default None
        Data type to force. Only a single dtype is allowed. If None, infer.
    copy : bool or None, default None
        Copy data from inputs.
        For dict data, the default of None behaves like `copy=True`.  For DataFrame
        or 2d ndarray input, the default of None behaves like `copy=False`.

        .. versionchanged:: 1.3.0

Difference Between Docstrings and Code Comments

After we ‘ve seen what docstrings look like, it’s time to learn why and how they differ from regular code comments. The main idea is that they (usually) serve different purposes.

Docstrings explain what a function/class is needed for (i.e., its description, arguments, and output — and any other useful information) while comments explain what specific code strings do. In other words, code comments are for people who want to modify the code, and docstrings are for people who want to use the code.

In addition, code comments may be useful when planning the code (for example, by implementing pseudocode, or leaving temporary comments with your ideas or thoughts that aren’t intended for an end-user).

Docstring Formats

Let’s look at the different types of docstrings. First, it’s an excellent idea to browse Python’s PEP pages. We can find PEP 257, which summarizes Python docstrings. I strongly recommend you read it all even though you may not understand all of it. The essential points are as follows:

  1. Use triple double quotes to enclose docstrings.
  2. Docstring ends with a dot.
  3. It should describe the command of the function (i.e., what the function does, so we usually begin the phrase with "Return…").
  4. If we need to add more information (for instance, about arguments), then we should leave a blank line between the summary of the function/class and a more detailed description (more about it will follow later in the tutorial).
  5. Multi-line docstrings are fine if they increase readability.
  6. There should be a description of arguments, output, exceptions, etc.

Note that these are just recommendations and not strict rules. For example, I doubt we would need to add more information about the arguments or output to the sum_subtract() function because the summary is already descriptive enough.

There are four primary types of docstrings, all of which follow the above recommendations:

  1. NumPy/SciPy docstrings
  2. Google docstrings
  3. reStructuredText
  4. Epytext

You can look at all of them and decide which one you like the most. NumPy/SciPy and Google docstrings will appear more frequently even though reStructuredText is the official Python documentation style.

Let’s look at a real-world dataset and write a function to apply to one of its columns.

This dataset contains top video games on Metacritic in 1995-2021.

from datetime import datetime
import pandas as pd

all_games = pd.read_csv("all_games.csv")
print(all_games.head())
                                   name        platform        release_date  \
0  The Legend of Zelda: Ocarina of Time     Nintendo 64   November 23, 1998   
1              Tony Hawk's Pro Skater 2     PlayStation  September 20, 2000   
2                   Grand Theft Auto IV   PlayStation 3      April 29, 2008   
3                           SoulCalibur       Dreamcast   September 8, 1999   
4                   Grand Theft Auto IV        Xbox 360      April 29, 2008   

                                             summary  meta_score user_review  
0  As a young boy, Link is tricked by Ganondorf, ...          99         9.1  
1  As most major publishers' development efforts ...          98         7.4  
2  [Metacritic's 2008 PS3 Game of the Year; Also ...          98         7.7  
3  This is a tale of souls and swords, transcendi...          98         8.4  
4  [Metacritic's 2008 Xbox 360 Game of the Year; ...          98         7.9  

For example, we may be interested in creating a column that computes how many days ago a game was released. For this purpose, we will also use the datetime package (tutorial one, tutorial two).

First, let’s write this function and test it:

def days_release(date):
    current_date = datetime.now()
    release_date_dt = datetime.strptime(date, "%B %d, %Y") # Convert date string into datetime object
    return (current_date - release_date_dt).days

print(all_games["release_date"].apply(days_release))
0        8626
1        7959
2        5181
3        8337
4        5181
         ... 
18795    3333
18796    6820
18797    2479
18798    3551
18799    4845
Name: release_date, Length: 18800, dtype: int64

The function works as intended, but it’s better to complement it with docstrings. First of all, let’s use the SciPy/NumPy format:

def days_release(date):
    """
    Return the difference in days between the current date and game release date.

    Parameter
    ---------
    date: str
        Release date in string format.

    Returns
    -------
    int64
        Integer difference in days.
    """
    current_date = datetime.now()
    release_date_dt = datetime.strptime(date, "%B %d, %Y") # Convert date string into datetime object
    return (current_date - release_date_dt).days

Above in the Returns section, I didn’t repeat (most of) what is already in the summary section.

Next, a Google docstrings example:

def days_release(date):
    """Return the difference in days between the current date and game release date.

    Args:
        date (str): Release date in string format.

    Returns:
        int64: Integer difference in days.
    """
    current_date = datetime.now()
    release_date_dt = datetime.strptime(date, "%B %d, %Y") # Convert date string into datetime object
    return (current_date - release_date_dt).days

A reStructuredText example:

def days_release(date):
    """Return the difference in days between the current date and game release date.

    :param date: Release date in string format.
    :type date: str
    :returns: Integer difference in days.
    :rtype: int64
    """
    current_date = datetime.now()
    release_date_dt = datetime.strptime(date, "%B %d, %Y") # Convert date string into datetime object
    return (current_date - release_date_dt).days

Finally, an Epytext example:

def days_release(date):
    """Return the difference in days between the current date and the game release date.
    @type date: str
    @param date: Release date in string format.
    @rtype: int64
    @returns: Integer difference in days.
    """
    current_date = datetime.now()
    release_date_dt = datetime.strptime(date, "%B %d, %Y") # Convert date string into datetime object
    return (current_date - release_date_dt).days

Most function docstrings should have at least a summary of their functionality, input, and output description. Furthermore, if they’re more complicated, they may include examples, notes, exceptions, etc.

Finally, I didn’t talk much about classes because they aren’t very frequent in data science, but in addition to what their methods (functions) include, they should also have the following:

  1. Attributes descriptions
  2. List of methods and summary of their functionality
  3. Default values of attributes

Script Docstrings

Python docstrings also describe the functionality and usage guidelines of small scripts, which may be appropriate for the automation of some of our daily tasks. For example, I frequently need to convert Danish kroner into euros. I can type a query on Google each time, or I can download an app that can do it for me, but it all seems overly complicated and redundant. I know that one euro is roughly 7.45 Danish krone, and since I almost always work in a Linux terminal, I decided to write a small CLI program that converts one currency into the other:

"""
DKK-EUR and EUR-DKK converter.

This script allows the conversion of Danish kroner to euros and euros to Danish kroner with a fixed exchange rate
of 7.45 Danish krone for one euro.

It is required to specify the conversion type: either dkk-eur or eur-dkk as well as the type of the input currency
(EUR or DKK).

This file contains the following functions:

    * dkk_converter - converts Danish kroner to euros
    * eur_converter - converts euros to Danish kroner
    * main - main function of the script
"""

import argparse

# Create the parser
parser = argparse.ArgumentParser(
    prog="DKK-EUR converter", description="Converts DKK to EUR and vice versa"
)

# Add arguments
parser.add_argument("--dkk", "-d", help="Danish kroner")
parser.add_argument("--eur", "-e", help="Euros")
parser.add_argument(
    "--type",
    "-t",
    help="Conversion type",
    choices=["dkk-eur", "eur-dkk"],
    required=True,
)

# Parse the arguments
args = parser.parse_args()

def dkk_converter(amount):
    """Convert Danish kroner to euros."""
    amount = float(amount)
    print(f"{amount} DKK is {round(amount / 7.45, 2)} EUR.")

def eur_converter(amount):
    """Convert euros to Danish kroner."""
    amount = float(amount)
    print(f"{amount} EUR is {round(amount * 7.45, 2)} DKK.")

def main():
    """Main function."""
    if args.type == "dkk-eur":
        dkk_converter(args.dkk)
    elif args.type == "eur-dkk":
        eur_converter(args.eur)
    else:
        print("Incorrect conversion type")

main()

The script should be sufficiently documented to allow the user to apply it. At the top of the file, a docstring should describe the main purpose of the script, brief guidelines, and functions or classes it contains. In addition, if any third-party package is used, it should be stated in docstrings, so that the user installs it prior to using the script.

If you use the argparse module to create CLI applications, each of its arguments should be described in the help menu, so that the end user can choose the -h option and determine the input. Furthermore, the description parameter should be filled in within the ArgumentParser object.

Finally, all functions should be properly documented as described previously. Here, I omitted the "Arguments" and "Returns" descriptors because, from my perspective, they’re unnecessary.

Also, note how I used the code comments and how they are different from docstrings. If you have a script you wrote, add to it some documentation to practice.

General Advice on How to Write Python Docstrings and Documentation

It should be pretty clear why documentation is important and why docstrings are an essential part of Python documentation. To conclude, let me give you some general advice on Python docstrings and documentation.

  1. Write documentation for people, not computers. It should be descriptive but concise and straightforward.
  2. Don’t overuse docstrings. Sometimes they aren’t necessary, and a small code comment may suffice (or even no comment at all). Assume that the developer has some basic knowledge of Python.
  3. Docstrings shouldn’t explain how the function works under the hood but, rather, how to use it. Sometimes, it may be necessary to explain the inner mechanism of a piece of code, so use code comments for that.
  4. Don’t use docstrings instead of comments, and comments instead of code.

Summary

Here is what we learned in this tutorial:

  1. Documentation is an essential part of a Python project — it’s important for end users, developers, and you.
  2. Docstrings are for using the code, and comments are for modifying the code.
  3. PEP 257 summarizes Python docstrings.
  4. There are four primary docstring formats: NumPy/SciPy docstrings, Google docstrings, reStructuredText, and Epytext. The first two are the most common.
  5. A script docstring should describe the functionality of the script, its usage, and functions contained within it.

I hope that you have learned something new today. Feel free to connect with me on LinkedIn or GitHub. Happy coding!

Artur Sannikov

About the author

Artur Sannikov

I am a Molecular Biology student at the University of Padua, Italy interested in bioinformatics and data analysis.

Learn data skills for free

Headshot Headshot

Join 1M+ learners

Try free courses