Lone Learner
Lone Learner

Reputation: 20698

How to express multiple types for a single parameter or a return value in docstrings that are processed by Sphinx?

Sometimes a function in Python may accept an argument of a flexible type. Or it may return a value of a flexible type. Now I can't remember a good example of such a function right now, therefore I am demonstrating what such a function may look like with a toy example below.

I want to know how to write docstrings for such functions using the Sphinx documentation notation. In the example below, the arguments may be either str or int. Similarly it may return either str or int.

I have given an example docstrings (both in the default Sphinx notation as well as the Google notation understood by Sphinx's napoleon extension). I don't know if this is the right way to document the flexible types.

Sphinx default notation:

def add(a, b):
    """Add numbers or concatenate strings.

    :param int/str a: String or integer to be added
    :param int/str b: String or integer to be added
    :return: Result
    :rtype: int/str
    """
    pass

Sphinx napoleon Google notation:

def add2(a, b):
    """Add numbers or concatenate strings.

    Args:
      a (int/str): String or integer to be added
      b (int/str): String or integer to be added

    Returns:
      int/str: Result
    """
    pass

What is the right way to express multiple types for parameters or return values in docstrings that are meant to be processed by Sphinx?

Upvotes: 60

Views: 61931

Answers (1)

Python 3.10 | (pipe, binary or) Union type hint syntax sugar

Once you get access, this will be the way to go, it is sweet:

def f(i: int|str) -> int|str:
    if type(i) is str:
        return int(i) + 1
    else:
        return str(i)

The PEP: https://peps.python.org/pep-0604/

Documented at: https://docs.python.org/3.11/library/typing.html#typing.Union

Union type; Union[X, Y] is equivalent to X | Y and means either X or Y.

Python 3.5 Union type hints

https://docs.python.org/3/library/typing.html#typing.Union

from typing import Union

def f(i: Union[int,str]) -> Union[int,str]:
    if type(i) is str:
        return int(i) + 1
    else:
        return str(i)

What to do before you get access to typing

For the poor souls stuck in older Pythons, I recommend using the exact same syntax as that Python 3 module, which will:

  • make porting easier, and possibly automatable, later on
  • specifies a unique well defined canonical way to do things

Example:

def f(i: Union[int,str]) -> Union[int,str]:
    """
    :param i: Description of the parameter
    :type i: Union[int,str]
    :rtype: Union[int,str]
    """
    if type(i) is str:
        return int(i) + 1
    else:
        return str(i)

or syntax

While reading through the docs I found another recommendation, now likely fully obsoleted by Union which also just works, but which might work on even older sphinx https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists

Multiple types in a type field will be linked automatically if separated by the word “or”:

:type an_arg: int or None
:vartype a_var: str or int
:rtype: float or str

typing.Optional: optional arguments

As mentioned in this comment, Optional is a synonym to Union[SomeType,None], e.g.:

from typing import Optional

def maybe_i(i: Optional[int] = None) -> int:
    if i is None:
        return 0
    return i + 1

assert maybe_i() == 0
assert maybe_i(1) == 2

However, with the introduction of |, perhaps the scales have shifted in favor of SomeType|None which both golfs better (5 chars vs 11 chars, if you don't spaces around |) and is more explicit:

def maybe_i(i: int|None = None) -> int:
    if i is None:
        return 0
    return i + 1

assert maybe_i() == 0
assert maybe_i(1) == 2

Sphinx support

Sphinx supports both typing and :type x: Union[int,str] well now. Example:

requirements.txt

Sphinx==4.5.0

main.py

from typing import Optional, Union

class C:
    '''
    My doc for C!
    '''
    pass

class D:
    '''
    My doc for D!
    '''
    pass

def main(i: Union[C, D]) -> Union[C, D]:
    '''
    My doc for main!

    :param i: My doc for i!
    '''
    return C()

def main_docstring(i):
    '''
    My doc for main_docstring!

    :param i: My doc for i!
    :type i: Union[C, D]
    :rtype: Union[C, D]
    '''
    return C()

def main_optional(i: Optional[C]) -> Optional[C]:
    '''
    My doc for main_optional!
    '''
    return None

def main_optional_docstring(i):
    '''
    My doc for main_optional_docstring!

    :param i: My doc for i!
    :type i: Optional[C]
    :rtype: Optional[C]
    '''
    return None

conf.py

import os
import sys
sys.path.insert(0, os.path.abspath('.'))
extensions = [ 'sphinx.ext.autodoc' ]
#autodoc_typehints = "description"

index.rst

.. automodule:: main
    :members:

Build with:

sphinx-build . out

Now:

xdg-open out/index.html

contains:

enter image description here

and all type links work just fine. Also note how it automatically uses the nicer pipe notation even if we wrote Union[].

One thing to note is that the types set with typing syntax show next to the argument, while those set with :type: show on the description.

We can make everything show on the description by uncommenting on conf.py as mentioned at Python 3: Sphinx doesn't show type hints correctly

autodoc_typehints = "description"

which gives:

enter image description here

but it would be even better if we could instead do it the other way around and show :type: next to the arguments. Anyways, both are acceptable.

typing.Protocol: enter polymorphism

Union is usually a code smell. For small stuff it is OK. But saner APIs will instead use polymorphism when possible: How to implement virtual methods in Python?

And now typing also offers static polymorphism check with Protocol, e.g.:

from typing import Protocol

class CanFly(Protocol):
    def fly(self) -> str:
        raise NotImplementedError()

class Bird(CanFly):
    def fly(self):
        return 'Bird.fly'

class Bat(CanFly):
    def fly(self):
        return 'Bat.fly'

def send_mail(flyer: CanFly):
    print(flyer.fly())

send_mail(Bird())
send_mail(Bat())

So here send_mail can take any type that implements CanFly, e.g. either Bird() or Bat(), and we don't need any ugly if type checks.

Upvotes: 86

Related Questions