Reputation: 20698
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
Reputation: 384234
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 toX | Y
and means eitherX
orY
.
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:
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:
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:
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