Skip to article frontmatterSkip to article content

Docstrings and formatting code

The University of Texas at Austin

Formatting: Formatting Python code involves making changes that improve readability to human users. These are changes that don’t necessarily change the logic of the program itself, but which make the code easier to read and understand. One does not necessarily need to focus on refactoring code, in which we change the logic of the program itself in order to make the code easier to extend, troubleshoot, or modify, although refactoring can certainly help with human readability. Python has a strong set of conventions regarding formatting, PEP-8, which can help guide formatting code.

We’ve seen that whitespace in Python has meaning: Code written without proper indentation within loops, for example, will fail to run. We’ve also seen that comments are useful for explaining the code to human readers.

Docstrings

Docstrings describe the structure and attributes of Python functions and classes. These have special treatment in most Python environments. For example, docstrings are printed out whenever the command help(name_of_object) is run, and some IDE like VSCode render docstrings in a popup when your cursor hovers over a function.

There are lots of different conventions for formatting docstrings, depending on preferences. For generality, it is usually recommended to use one of the Napolean supported docstring formats. The Google docstring format uses less vertical space at the expense of longer horizontal descriptions, while the NumPy format uses more verticle space, but has shorter lines. We will use the Google format throughout this tutorial. If you generally write code in a Terminal or other settings where horizonal scanning is difficult, then the NumPy format may be preferred.

Docstrings

Docstrings describe the structure and attributes of Python functions and classes. These have special treatment in most Python environments. They are printed out whenever the command help(name_of_object) is run, and some IDE like VSCode render docstrings in a popup when your cursor hovers over a function.

There are lots of different conventions for formatting docstrings, depending on preferences. For generality, I usually recommend using one of the Napolean supported docstring formats. The Google docstring format uses less vertical space at the expense of longer horizontal descriptions, while the Numpy format uses more verticle space, but has shorter lines. I generally prefer less scrolling, and so I’ll use the Google format throughout this tutorial. If you generally write code in a Terminal or other settings where horizonal scanning is difficult, then the numpy format may be preferred.

Here’s an example of a function that computes each step of the dynamics of a particle in a potential well. The docstring briefly describes the function, and then lists the arguments, their types, and meaning. The return value is also described.

    def ornstein_uhlenbeck(n, ic=0.0, x0=0):
        """
        Simulate the motion of a particle in a one-dimensional harmonic potential well
        with Brownian forcing

        Args:
            n (int): The number of steps to simulate
            ic (float): The initial value of the process
            x0 (float): The location of the potential minimum

        Returns:
            traj (list): The n steps of the dynamical process

        """
        curr = ic
        traj = list()
        for _ in range(n):
            traj.append(curr)
            curr = (curr - x0) + 0.1 * random.random()
        return traj

The syntax of the docstring is usually rather strict. We declare the type of each argument, and the type of the return value. We also include a description of the function, and the meaning of each argument. This explicit, rigid structure is useful for other users of your code, and also for your future self. If you come back to a function you wrote a year ago, you’ll have a description of what it does. The strict syntax also helps the computer format the docstring when the help command is invoked, and it will also make it possible to automatically generated online documentation for code using an autodoc tool.

Docstring entries

The docstring for a function should always include the following entries:

Additionally, you might find it useful to include the following entries:

Docstrings for classes

For classes, we usually place the appropriate docstring at the top level, rather than within the __init__ method. However, placing documentation in the top-level class versus the __init__ method is a matter of convention, and both are valid. However, in most environments, the help command will print out the docstring for both the class and the __init__ method when it is called on the class itself, and not just when it is called on an instance of the class. Here is an example of a docstring for a class that simulates the dynamics of a particle in a potential well:

class OrnsteinUhlenbeck:
    """
    A class for simulating the motion of a particle in a one-dimensional harmonic potential well
    with Brownian forcing

    Args:
        x0 (float): The location of the potential minimum. Defaults to 0.

    Attributes:
        x0 (float): The location of the potential minimum
        traj (list): The trajectory of the particle

    Methods:
        step: Simulate a single step of the process
        run: Simulate the process for a given number of steps

    Examples:
        >>> ou = OrnsteinUhlenbeck()
        >>> ou.run(10)
        >>> ou.traj
        [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]

    Notes:
        This implementation uses a Gaussian random number generator to simulate the
        discrete-time effects of Brownian forcing.
        
    """
    def __init__(self, x0=0):
        self.x0 = x0
        self.traj = list()

    def step(self):
        """
        Take a single step of the dynamical process

        Returns:
            float: The new value of the process

        """
        if len(self.traj) == 0:
            curr = 0.0
        else:
            curr = self.traj[-1]
        self.traj.append((curr - self.x0) + 0.1 * random.random())
        return self.traj[-1]

    def run(self, n):
        """
        Simulate the process for a given number of steps

        Args:
            n (int): The number of steps to simulate

        Returns:
            list: The n steps of the dynamical process

        """
        for _ in range(n):
            self.step()
        return self.traj

The docstring for the class itself should include a description of the class, and a description of each attribute. The docstring for the __init__ method should include a description of the class, and a description of each argument. The docstring for class-specific methods like the step method should include a description of the method, and a description of the return value, just as for functions.

Some common fields in a class-level docstring are:

Formatting code

In Python, the formatting of the code can convey literal meaning to the interpreter. For example, the following code will not run:

    def f(x):
    return x + 1

    f(1)

The reason is that the second line is not indented, and so the interpreter interprets it as a continuation of the first line. The spaces that shoudl indent the second line thus have direct meaning in Python, in contrast to other languages like R, C, or Java.

Beyond the literal meaning of the code, there are also conventions for formatting code that make it easier to read and understand. These sets of conventions are called PEP-8. Among the most important are:

For example, the following code follows PEP 8 conventions:

def f(x: int) -> int:
    """Return x + 1."""
    return x + 1


if __name__ == "__main__":
    print(f(1))

Here, the function is indented consistently, type annotations and a docstring are provided for clarity, and the if name == “main”: guard is used to make the file runnable as a script while still allowing imports. Tools like black, isort, and flake8 can automatically format code to conform to PEP 8 conventions. Black is a particularly popular tool for formatting code that invokes several additional formatting conventions, and many IDEs like VSCode and Jupyter have built-in plugins that can automatically format code with black.