How to display an argument as optional
Is your feature request related to a problem? Please describe. I am using autodoc_typehints = "description" (and napoleon) in conf.py to add type hints in the rendered docstring. Then the following docstring
def func(param1: int, param2: int = 0):
"""...
Args:
param1: description1
param2: description2
"""
is rendered like this:
param1 (int): description1
param2 (int): description2
So far so good. But next I would like my rendered docstring to display whether a parameter is optional, i.e. has a default. In other words I want it to look like this:
param1 (int): description1
param2 (int, optional): description2
Describe the solution you'd like Is there a way to achieve this automatically?
Describe alternatives you've considered When I type
def func(param1: int, param2: int = 0):
"""...
Args:
param1: description1
param2(optional): description2
"""
the automatically generated typehint is overwritten:
param1 (int): description1
param2 (optional): description2
Additional context I already opened an issue in autoapi, but they redirected me to here.
AFAIK, this is not yet supported. The reason is that (optional) is treated as a type itself and rendering the type annotation only works if there is no type yet. What you want is to merge the type hints and the type description but this may lead to some issues:
- If two types are equivalent but named differently, there will be a duplication.
- What should we do if the type annotation and the description are incompatible ?
One possibility instead is to modify the behaviour of autodoc_typehints as follows:
- If there is a default value and a type annotation, then Sphinx automatically adds the "optional". That way, you won't even need to put
(optional). If you however put it, we won't duplicate it (i.e., "optional, optional" will collapse to "optional"). - If the type description is not explicitly "(optional)", we keep the current behaviour.
Related: #11376.
If there is a default value and a type annotation, then Sphinx automatically adds the "optional". That way, you won't even need to put (optional). If you however put it, we won't duplicate it (i.e., "optional, optional" will collapse to "optional").
Is exactly what I would like. But I do not understand
If the type description is not explicitly "(optional)", we keep the current behaviour.
I would like to have the docstring of
def func(param1: int, param2: int = 0):
"""...
Args:
param1: description1
param2: description2
"""
rendered as
param1 (int): description1
param2 (int, optional): description2
In other words: I never want to write anything explicitly in the docstring. Both the type - e.g. int - and optional should automatically be added in the rendered docs based on the function's signature.
I don't remember why I said
If the type description is not explicitly "(optional)", we keep the current behaviour.
I think it was an answer to the alternative you considered but I put it in the bullet list itself. As for what you actually want, it should be the first bullet point. I think it should be easy enough to actually add the optional properly by changing this:
https://github.com/sphinx-doc/sphinx/blob/d3c91f951255c6729a53e38c895ddc0af036b5b9/sphinx/ext/autodoc/typehints.py#L31-L35
and
https://github.com/sphinx-doc/sphinx/blob/d3c91f951255c6729a53e38c895ddc0af036b5b9/sphinx/ext/autodoc/typehints.py#L152-L155
When recording type hints, we would also record whether there is an optional value and then we would put some optional in the node, if needed.
Note for myself: only add , optional) if the node does not already ends with , optional).
Can you estimate when this could be done?
I am currently travelling and am quite busy until late September. In addition, the PR needs to be approved but the current owner is also extremely busy. I should be able to make a PR either by the end of August, or in October but I have no ides how long it will take to merge it upstream.
If anyone wants to pick the task, they can. I don't call dibs on this one!
Are there any updates? This is still relevant.
Unfortunately I don't have time for that. I know it's relevant but this kind of issje is likely to stay opened until someone is willing to help (we lack contributors so help is always welcomed).
Also, I would like to know the opinion of other maintainers to know whether it's worth our time or not for the moment.
@picnixz This is somewhat of a less common syntax (and perhaps older if not legacy? Predating the current state of the typing module):
param2 (int, optional): description2
Here optional doesn't express the None type as in param2 (Optional[int]): description2 in Python 3.10< or as param2 (int | None): description2 in Python 3.10+. Parameters with default values in a signature are implicitly "optional" (optional being plain English unrelated to the None type).
The Sphinx docs still have a few scattered examples of this kind of syntax (for example here) but where I think it's seen most is in the numpydoc context. I have to wonder if this syntax is still used or if it should be considered legacy altogether (and a poor practice at present, that's especially prone to add confusion for beginners), because reading through the PEPs and numpydoc I can't find a solid reference or need for it in 2024.
I did not consult any PEPs, but why do you think, it's a poor practise or even deprecated when numpy (one of the most popular Python packages) uses this style, see e.g. numpy.trace
Personally, I think that this style adds clarity rather than confusion - especially for beginners.
when numpy (one of the most popular Python packages) uses this style
NumPy uses its namesake documentation style, but writing the types as list of int in prose is only understandable so long as the types are short. Most of the NumPy API uses flat collections with only a few builtin type but APIs with more complex types e.g. (Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[[object]]]]]) of pytest.fixture wouldn't be readable using the NumpPy style. In other words, while the style is common in Python's scientific libraries, it's because for the particular and quite narrow case of flat and homogeneous collections with numerical data types it works just well enough to still be readable.
I did not consult any PEPs
The use of the optional keyword is motivated by a single sentence in PEP 257 with no clear indication of how it should be done.
PEP 257 – Docstring Conventions
Optional arguments should be indicated.
In fact, if you look at the NumPy documentation the use of optional/default is often inconsistent throughout the library (the linked example is one of the few cases where default is used but most times it's omitted), There's no clear mention or explanation of optional/default at least not one that can be quickly found, and it's also absent from the Google Style guide that NumPy follows.
Personally, I think that this style adds clarity rather than confusion
What it adds is duplication of the signature in prose. Saying something twice doesn't make it clearer. I haven't found one single consistent explanation in any of the style guides out there of where, how and why optional/default should be used, if it's use might marginally help beginner users it certainly adds confusion through ambiguity for documentation authors.
This issue is solely about adding the word optional next to type in the rendered docstring, see my first post in this conversation.
Hence, I do not understand the point about long type names. However, you've got a point about the duplication with the function signature. Let me digest this. ;) If I do not come up with another argument, I will close this issue presently.
@liebsc21 leave the issue open. I made the argument because I felt it needed to be made. What I think is needed is a brief context in the Sphinx docs and the NumPy docs putting the use of optional/default into perspective as I did here, but that is an argument for another post not this one.
When you say:
Let me digest this.
This stuff should be made obvious for documentation authors the first time they read the Napoleon/NumPy docs.
There is a case for having the ability to specify optional.
This would be the way I would see it working.
class DataManager:
...
def set_data(self, **kwargs) -> dict:
"""Method to set specific data for a request.
:param headers: If True return headers as part of the data.
:type headers: bool
:optional headers: true
# If optional is false, or omitted then it's required
:param body: The body contents of the request.
:type body: str
# Here optional is omitted so 'body' is required.
"""
data: dict = {}
if 'headers' in kwargs.keys():
headers = """Some code to build headers goes here"
data['headers'] = headers
...
return data