Describing code in Sphinx

In the previous sections of the tutorial you can read how to write narrative or prose documentation in Sphinx. In this section you will describe code objects instead.

Sphinx supports documenting code objects in several languages, namely Python, C, C++, JavaScript, and reStructuredText. Each of them can be documented using a series of directives and roles grouped by domain. For the remainder of the tutorial you will use the Python domain, but all the concepts seen in this section apply for the other domains as well.

Documenting Python objects

Sphinx offers several roles and directives to document Python objects, all grouped together in the Python domain. For example, you can use the py:function directive to document a Python function, as follows:

docs/source/usage.rst
Creating recipes
----------------

To retrieve a list of random ingredients,
you can use the ``lumache.get_random_ingredients()`` function:

.. py:function:: lumache.get_random_ingredients(kind=None)

   Return a list of random ingredients as strings.

   :param kind: Optional "kind" of ingredients.
   :type kind: list[str] or None
   :return: The ingredients list.
   :rtype: list[str]

Which will render like this:

HTML result of documenting a Python function in Sphinx

The rendered result of documenting a Python function in Sphinx

Notice several things:

  • Sphinx parsed the argument of the .. py:function directive and highlighted the module, the function name, and the parameters appropriately.

  • The directive content includes a one-line description of the function, as well as a info field list containing the function parameter, its expected type, the return value, and the return type.

Note

The py: prefix specifies the domain. You may configure the default domain so you can omit the prefix, either globally using the primary_domain configuration, or use the default-domain directive to change it from the point it is called until the end of the file. For example, if you set it to py (the default), you can write .. function:: directly.

Cross-referencing Python objects

By default, most of these directives generate entities that can be cross-referenced from any part of the documentation by using a corresponding role. For the case of functions, you can use py:func for that, as follows:

docs/source/usage.rst
The ``kind`` parameter should be either ``"meat"``, ``"fish"``,
or ``"veggies"``. Otherwise, :py:func:`lumache.get_random_ingredients`
will raise an exception.

When generating code documentation, Sphinx will generate a cross-reference automatically just by using the name of the object, without you having to explicitly use a role for that. For example, you can describe the custom exception raised by the function using the py:exception directive:

docs/source/usage.rst
.. py:exception:: lumache.InvalidKindError

   Raised if the kind is invalid.

Then, add this exception to the original description of the function:

docs/source/usage.rst
.. py:function:: lumache.get_random_ingredients(kind=None)

   Return a list of random ingredients as strings.

   :param kind: Optional "kind" of ingredients.
   :type kind: list[str] or None
   :raise lumache.InvalidKindError: If the kind is invalid.
   :return: The ingredients list.
   :rtype: list[str]

And finally, this is how the result would look:

HTML result of documenting a Python function in Sphinx with cross-references

HTML result of documenting a Python function in Sphinx with cross-references

Beautiful, isn’t it?

Including doctests in your documentation

Since you are now describing code from a Python library, it will become useful to keep both the documentation and the code as synchronized as possible. One of the ways to do that in Sphinx is to include code snippets in the documentation, called doctests, that are executed when the documentation is built.

To demonstrate doctests and other Sphinx features covered in this tutorial, Sphinx will need to be able to import the code. To achieve that, write this at the beginning of conf.py:

docs/source/conf.py
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here.
import pathlib
import sys
sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix())

Note

An alternative to changing the sys.path variable is to create a pyproject.toml file and make the code installable, so it behaves like any other Python library. However, the sys.path approach is simpler.

Then, before adding doctests to your documentation, enable the doctest extension in conf.py:

docs/source/conf.py
extensions = [
    'sphinx.ext.duration',
    'sphinx.ext.doctest',
]

Next, write a doctest block as follows:

docs/source/usage.rst
>>> import lumache
>>> lumache.get_random_ingredients()
['shells', 'gorgonzola', 'parsley']

Doctests include the Python instructions to be run preceded by >>>, the standard Python interpreter prompt, as well as the expected output of each instruction. This way, Sphinx can check whether the actual output matches the expected one.

To observe how a doctest failure looks like (rather than a code error as above), let’s write the return value incorrectly first. Therefore, add a function get_random_ingredients like this:

lumache.py
def get_random_ingredients(kind=None):
    return ["eggs", "bacon", "spam"]

You can now run make doctest to execute the doctests of your documentation. Initially this will display an error, since the actual code does not behave as specified:

(.venv) $ make doctest
Running Sphinx v4.2.0
loading pickled environment... done
...
running tests...

Document: usage
---------------
**********************************************************************
File "usage.rst", line 44, in default
Failed example:
    lumache.get_random_ingredients()
Expected:
    ['shells', 'gorgonzola', 'parsley']
Got:
    ['eggs', 'bacon', 'spam']
**********************************************************************
...
make: *** [Makefile:20: doctest] Error 1

As you can see, doctest reports the expected and the actual results, for easy examination. It is now time to fix the function:

lumache.py
def get_random_ingredients(kind=None):
    return ["shells", "gorgonzola", "parsley"]

And finally, make test reports success!

For big projects though, this manual approach can become a bit tedious. In the next section, you will see how to automate the process.