Documenting your way to better tests in Python
Tags: python, tdd, documentation, docstring
Published: 2021-05-14 04:42:28

## Writing and Reading Code

> Writing code is hard, reading code is harder

I participate in a lot of code reviews and one thing I've realized is - developers spend most of their time coding a solution, not enough time explaining it. 

There are two ways you could explain how your code works:
1. Write simple code that is self-explanatory
2. Write "good" documentation

## Value of Good Documentation

In this post, I am going to make a case for how writing good documentation can improve the quality of your code and should be done **along with** writing self-explanatory code. 

Let me illustrate with an example.

I've defined `my_function` below, that computes some "arbitrary" logic:
``` python 
def my_function(input_string, input_list=None):
    head = "Start:"
    tail = "" if not input_list else " ".join(input_list)
    output = f"{input_string} {tail}"
    if not input_string.startswith(head):
        output = f"{head} {input_string} {tail}"
        
    return output.strip()
```

> In a real-world problem, this piece of code might be much more complex. 

As a reader, you could read this code and understand what it is doing. However, I can make the reader's job easier by using [docstring](https://www.python.org/dev/peps/pep-0257/#what-is-a-docstring).  

## Docstrings: Make code easy to read

Docstring holds immense value when developing code in a large organization and/or in a collaborative community. 

It lets any developer understand what's happening in the function/module without them having to read through the entire codebase. 

It can also be used with tools like [sphinxdoc](https://www.sphinx-doc.org/en/master/) to generate beautiful documentation for your project.

``` python
def my_function(input_string, input_list=None):
    """Sanitize input string and append extra text if required
    
    The function checks if input_string starts with 'Start:' 
    if not, it will add the string to the input_string 
    
    It also converts input_list to a string using join 
    and appends to the input_string
    """
    head = "Start:"
    tail = "" if not input_list else " ".join(input_list)
    output = f"{input_string} {tail}"
    if not input_string.startswith(head):
        output = f"{head} {input_string} {tail}"
        
    return output.strip()
```

Docstring in the above code explains what the code is doing, but it still feels like a repetition of what is written in the code block. 

How do we improve this?
Using [doctest] (https://docs.python.org/3/library/doctest.html)! 

## Doctest: Read, Test and Document better  

[doctest] (https://docs.python.org/3/library/doctest.html) allows you to not only test interactive Python examples but also makes sure your documentation is up to date.

Let us take a look at improved `docstring` for the same function using `doctest` 

``` python
def my_function(input_string, input_list=None):
    """Sanitize input string and append extra text if required

    >>> my_function('hi')
    'Start: hi'

    >>> my_function('Start: some string')
    'Start: some string'

    >>> my_function('hi', ['other', 'item'])
    'Start: hi other item'

    :param input_string: string to sanitize
    :type input_string: str
    :param input_list: extra items to append, defaults to None
    :type input_list: list, optional
    :return: sanitized string
    :rtype: str
    """
    head = "Start:"
    tail = "" if not input_list else " ".join(input_list)
    output = f"{input_string} {tail}"
    if not input_string.startswith(head):
        output = f"{head} {input_string} {tail}"
        
    return output.strip()
```
Here, I have defined some input-output examples for the given function which illustrate what the function is doing. For instance:

``` python 
my_function('hi', ['other', 'item'])
```
should return:
``` python 
'Start: hi other item'
```

The above documentation tells developers/readers the following:

* What the function does
* Parameters and their types 
* Return value and its type 
* Expected behavior - describes input/output examples

---

## Conclusion

### Generating documentation 

I used [sphinxdoc](https://www.sphinx-doc.org/en/master/) to generate documentation for the above function:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e9ok89nm237pyezq4j3j.png)

### Practicing TDD
Writing documentation in Python also allows me to follow Test Driven Development, where I first define the behavior in `docstring` then write the code. 

`doctest` can be run by any testing framework like `unittest` or `pytest` 

``` bash
$ pytest --doctest-modules -vv top.py
=================== test session starts ======================
platform darwin -- Python 3.8.2, pytest-6.2.4 
-- /projects/virtualenvs/dev-to/bin/python3
cachedir: .pytest_cache
rootdir: /Users/chaitanyadwivedi/projects/dev-to
collected 1 item                                                                                                                                           

top.py::top.my_function PASSED                          [100%]

==================== 1 passed in 0.05s =======================
```