How to Write Beautiful Code Documentation


Writing code documentation is really important. More than anything else, documenting your code pushes you to use best practices, and organize your code in a logical way.

After a year of writing Python code for a living [1], I'm writing to share the documentation process that works best for me .

Sphinx

Many python project docs are hosted either on GitHub Pages or on Read the Docs. In either case, most of these docs were generated using Sphinx. Take a moment to install Sphinx on your system, since we will be using it later in this tutorial.

Create a GitHub repository

GitHub is going to be one of the easiest places to host your documentation. You could just as easily host your code on Read the Docs, but that adds another system into your workflow, and I like keeping things simple.

If you don't have a GitHub account, create one. Log into your GitHub account, and create a new repository. When it comes time to clone your repository, I like to keep my repositories somewhere in my Dropbox folder (but not too deep). This allows me to switch between several computers and pick up wherever I left off. For the purposes of this tutorail, let's suppose that I'm coding a repository called hardware under ~/Dropbox/digonnet/. Let's clone the repository into our Dropbox. You will want to change your username and repository name according to your needs:

mkdir -p ~/Dropbox/digonnet
cd ~/Dropbox/digonnet
git clone https://github.com/jondoesntgit/hardware

Creating the README

Writing a README.md file is a great way to provide a 'front-page' for your repository. It doesn't have to be anything fancy (we'll get there in a second). Just a quick overview of why your package exists, and perhaps links to any documentation.

Here's an example of a README.md file that I use:

Python Hardware Wrappers
========================

This repository contains python wrappers that can be used to access hardware
commonly used inside of the Digonnet lab at Stanford. For more documentation,
visit [http://github.jamwheeler.com/hardware](http://github.jamwheeler.com/hardware)

Creating Separate Folders for Code and Docs

I've tried a couple different folder hierarchy, but this is the structure that has become my favorite:

hardware/
├── LICENSE.md
├── README.md
├── docs
│   ├── .nojekyll
│   ├── Makefile
│   ├── conf.py
│   ├── index.rst
│   ├── make.bat
│   └── modules
│       ├── data_acquisition_units.rst
│       ├── function_generators.rst
│       ├── gyros.rst
│       ├── laser_diode_drivers.rst
│       ├── lock_in_amplifiers.rst
│       ├── optical_power_meters.rst
│       ├── oscilloscopes.rst
│       ├── rotation_stages.rst
│       └── spectrum_analyzers.rst
└── hardware
    ├── __init__.py
    ├── data_acquisition_units.py
    ├── function_generators.py
    ├── gyros.py
    ├── laser_diode_drivers.py
    ├── lock_in_amplifiers.py
    ├── optical_power_meters.py
    ├── oscilloscopes.py
    ├── rotation_stages.py
    └── spectrum_analyzers.py

The first folder that we will want to create is the folder that will hold all of the code. Name this the same as the repository (since this will be where the python package lives). Within this folder, you can create an optionally black __init__.py so that the Python langauge knows this is a package.

Next, create a folder called docs and cd into it. Then, we will run a program called sphinx-quickstart

mkdir -p ~/Dropbox/digonnet/hardware/docs
cd ~/Dropbox/digonnet/hardware/docs

Initializing Sphinx

Let's set up Sphinx with the following command:

sphinx-quickstart

Most of the default settings are fine. You'll probably want to override the defaults on the following:

  • project a name
  • author
  • set autodoc on,
  • set mathjax on if you want equations
  • turn githubpages on,
  • make a Makefile.

Next, let's update the table of contents in index.rst to contain at least the following:

.. toctree::
   :maxdepth: 2
   :caption: Modules
   :glob:

   modules/*

Then, create a folder to hold the documentation for each module:

mkdir ~/Dropbox/digonnet/hardware/docs/modules

For each module that you have in your code directory (sibling to the docs directory), you'll want to create a file for that module. In each file, simply add the following lines at the top, substituting the name for the module in the appropriate place. This directive tells autodoc to pull the documenation strings out of the python code and write them up here.

.. automodule:: hardware.laser_diode_drivers
 :members:

You don't have to do it this way, but I think it is the cleanest way to set up your documentation in this way. When you're done, it should look like the folder hierarchy I showed in an earlier section.

Configuring Sphinx

Next, there are a few special variables that we will want to edit inside of our Sphinx conf.py file under docs. Let's edit it.

Near the top, add some of the extensions in to get a bit more power out of autodoc:

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.todo',
    'sphinx.ext.coverage',
    'sphinx.ext.mathjax',
    'sphinx.ext.viewcode',
    'sphinx.ext.napoleon',
    'sphinx.ext.githubpages']

# Napoleon settings
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = False
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = False
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = True
napoleon_use_param = True
napoleon_use_rtype = False
napoleon_use_keyword = True

You can also configure custom links in the sidebar by adding this code:

html_sidebars = {
  '**': [
    'globaltoc.html',
    'searchbox.html',
  ]
}

Writing docstrings

Finally, we get to writing the actual documentation. Probably the best way to see how this is done correctly is to read other people's documenation. You can check out some of the source that I've written here. In essence, you are going to write a string between declarations and the code. The string will contain instructions on how to use the code. You can also write these docstrings at the top of your file to explain it's purpose.

There is no single standard that tells you how you have to write your docstrings. There are a couple types of parsers that will autoformat certain parts fo your docstrings. If you want to learn more about these, you check out these Google Docstrings and Numpy Docstrings examples.

Compiling

When we are in the docs directory, we can run our Makefile by

make html

This will build a website under the _build/html directory. In general, it's good practice to exclude this from version control, so edit your .gitignore file to exclude docs/_build/*

But we don't want our docs just to stay on our computer. We want to publish them to the web. GitHub will host our docstrings for free if we configure it in our repository settings. Under the repository options page, scroll down until you see a "GitHub Pages" setting. You'll want the source to be the "gh-pages branch" option.

On your local machine, create a new branch called gh-pages

git checkout --orphan gh-pages
git rm -rf .
rm '.gitignore'

In order to start this new branch, you'll need to have something inside of it.

echo "My Page" > index.html
git add index.html
git commit -a -m "First pages commit"
git push origin master

Next, let's hop over to our main branch again.

git checkout master

Let's edit our docs/Makefile in order to add an option to build our gh-pages. Insert this into your Makefile

.PHONY: gh-pages
.ONESHELL:
gh-pages:
   rm -rf /tmp/gh-pages
   cp -r $(BUILDDIR)/html /tmp/gh-pages
   git checkout gh-pages
   cd .. && rm -rf * && cp -r /tmp/gh-pages/* . && git add . && git commit -m "Updated gh-pages" && git push && git checkout master

Now, you should be able to type make gh-pages, and your shell will automatically grab any documents that are under your docs/_build/html directory, and send them over to GitHub for hosting. By default, the will be hosted on jondoesntgit.github.io/hardware, with your username and package name filled in. Feel free to run make html as many times as you wish until everything looks just right locally, and then send your pages over to GitHub with make gh-pages

[1]I get paid to do research. Python captures, analyzes, and plots all of my research data. In this sense, I write Python code for a living.