9

Related to: Formatting python docstrings for dicts.

What is the correct way to provide documentation for a dictionary that is passed as an argument to a function? Here is an example with styling that I made up (based on Google document style for Sphinx):

def class_function_w_dict_argument(self, T_in, c_temps):
    """ This calculates things with temperatures

    Notes:
        All temperatures should be given in Kelvin

    Args:
        T_in (float): Known temperature (K)
        c_temps (dict): Dictionary of component temperatures
            {T1 (float): temperature (K)
             T2 (float): temperature (K)
             T3 (float): temperature (K)
             T4 (float): temperature (K)
             T5 (float): temperature (K)}

    Returns:
        T_out (float): Calculated temperature
    """
Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
Ross W
  • 1,300
  • 3
  • 14
  • 24
  • 3
    Usually in that case you do not use dictionaries, but **use parameters**: `t1`, `t2`, etc. Such that the fact that the parameters is passed, is *by design*. – Willem Van Onsem Sep 21 '17 at 18:13
  • I have a different class that produces the formatted dictionary with the temperatures in. It would be nice to be able to pass that whole dictionary (or one that is user made) rather than split it up into it's individual components then pass each as an argument or class parameter. – Ross W Sep 21 '17 at 18:24
  • 1
    Are you looking for a particular syntax that is *required* by Sphinx? Otherwise, I don't see how this isn't primarily opinion-based. – chepner Sep 21 '17 at 19:14
  • @chepner Yes I was inquiring if there was a standard docstring format that should be used when passing dictionaries to functions. I just used Sphinx as an example. – Ross W Sep 22 '17 at 21:47
  • Consider using a `TypedDict` instance to properly type hint your dict arguments – mousetail Jul 30 '22 at 15:23
  • 1
    @WillemVanOnsem *Usually* you're correct, use parameters. However, in the case of generics, it is fairly common to see a dictionary taken as an argument which simply specifies options that the abstract class could not know about. When overriding such a function, this dictionary could take any number of parameters, so it is worth documenting them. – Kraigolas Jul 30 '22 at 15:26
  • You can pass your dictionary to named parameters using the splatty-splat operator (`**`). – Mad Physicist Aug 01 '22 at 20:16
  • 2
    This is an opinion-based question; how you write your code comments is up to you. OP did not ask about or indicate in any way that they are trying to adhere to a style guide such as PEP8 or other PEP formats. – TylerH Aug 30 '22 at 14:51
  • Someone already answered the frame challenge. No, you should document your methods, and if you want to make double sure that your data structure has a specific construction, use a class instead https://softwareengineering.stackexchange.com/q/223992/104338 – Braiam Aug 31 '22 at 10:56

2 Answers2

5

Preface

To my knowledge, there is no PEP for differentiating specific types of input parameters in docstrings. However, Python supports typing, which provides further context into—and explicit constraints on—input parameters.

Typing

I recommend using typing rather than using docstrings for this purpose, as it is very easy for the dictionary data shape to drift from what is being documented—and the only thing worse than no documentation is incorrect documentation.

Furthermore having to document this dictionary for every function which takes it as an input parameter is cumbersome—it is much more maintainable (and testable) to create and maintain a type that can be shared throughout the entire codebase.

Example

from typing import TypedDict, NewType

Temperature = NewType("Temperature", float)

class ComponentTemperatures(TypedDict):
    T1: Temperature
    T2: Temperature
    T3: Temperature
    T4: Temperature
    T5: Temperature

def class_function_w_dict_argument(T_in, c_temps: ComponentTemperatures):
  print(c_temps)


c_temps = ComponentTemperatures({
    "T1": Temperature(1.0),
    "T2": Temperature(1.1),
    "T3": Temperature(1.2),
    "T4": Temperature(1.3),
    "T5": Temperature(1.4),
})

class_function_w_dict_argument(0, c_temps)

To enforce the typing, check it with mypy:

$ mypy main.py 
Success: no issues found in 1 source file

Here is a repl.it I created with this a working example: https://replit.com/@pygeek1/python-typing

Notes

  1. A List rather than Dict may be the more appropriate type for this data.

  2. mypy type checking can be included in your linter and/or test suite.

  3. There are many variations to this solution that can be applied depending on your needs, such as allowing for an arbitrary number of Dict items, or type checking the dict key instead of hardcoding it.

References

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

https://peps.python.org/pep-0589/

http://mypy-lang.org

https://stackoverflow.com/a/26714102/806876

pygeek
  • 7,356
  • 1
  • 20
  • 41
  • 1
    "having to document this dictionary for every function which takes it as an input parameter is cumbersome" I agree, this solution seems very useful. It's worth noting though that when overriding an abstract method which takes a `dict` or a `Mapping` using a specific `TypedDict` instead violates the [Liskov Substitution Principle](https://stackoverflow.com/a/584732/11659881), which mypy complains about. – Kraigolas Aug 01 '22 at 21:51
  • @Kraigolas you could define the type broadly enough—or modify the architecture of the program—otherwise it may be violating LSP anyway. If the program is designed to work where the values are floats, and you change the values to be ints or strings, it will still break—just later than if you had type checked the parameters in the first place. – pygeek Aug 02 '22 at 13:41
  • The reason I bountied this question was because I was curious about what to do when I'm inheriting an abstract class and need to override a method which takes a `dict` / `Mapping` as an argument. As typing in Python isn't necessary regardless, I think violating LSP here isn't bad, because my overridden function would only take eg., `ComponentTemperatures` regardless. It is worth noting that in that use case you probably will be violating LSP, but it seems like the lesser of two evils. – Kraigolas Aug 02 '22 at 14:43
  • 1
    @Kraigolas you still can have the type annotations without enforcing them with mypy. You could also make the parameter optional. I wish you created another question and included an example using abstract classes—as you say that’s the primary reason why you bountied the question, because I believe I answered the original question. – pygeek Aug 02 '22 at 19:04
  • 1
    [PEP 287 - Docstring-Significant Features](https://peps.python.org/pep-0287/#docstring-significant-features) does indeed establish a convention for specifying identifiers and their types, it says: *"Markup that isolates a Python identifier and specifies its type: interpreted text with roles."*. You should edit the first paragraph in your post to avoid misleading readers. – bad_coder Aug 13 '22 at 09:38
  • Having said that the remainder of your post is also wrong because dictionaries (and not just TypeDict) have supported detailed typing since the typing module was introduced, the example in the question should be typed `dict[float, K]` and that's it. The question itself should be closed for not containing an [MRE](https://stackoverflow.com/help/minimal-reproducible-example) of the dictionary, just an ambiguous description. The use of `TypedDict` is also not intended as you represent it in the answer, etc... There are so many things wrong with this thread that they outnumber any accurate info. – bad_coder Aug 13 '22 at 09:46
0

When your design causes problems, it is often easier to change the design than to find a solution. I would posit that this is one such case.

Since the keys in the dictionary are not generic, I would assign them to keyword arguments. That way you do not have to jump through extra hoops to use the values in your function:

def class_function_w_dict_argument(self, T_in, T1=None, T2=None, T3=None, T4=None, T5=None, **kwargs):
    """ This calculates things with temperatures

    Notes:
        All temperatures should be given in Kelvin

    Args:
        T_in (float): Known temperature (K)
        T1 (float): temperature (K)
        T2 (float): temperature (K)
        T3 (float): temperature (K)
        T4 (float): temperature (K)
        T5 (float): temperature (K)

    Returns:
        T_out (float): Calculated temperature
    """

The **kwargs at the end of the argument list allows you to pass in dictionaries that contain more than the required keys without having to worry about the extra keys:

some_dict = {'T1': 10.0, 'T2': 11.0, 'T3': 13.0, 'other_key': 'nope'}
some_instance.class_function_w_dict_argument(**some_dict)

If your dictionary must contain values for all components, then simply removing =None in the defined argument list will be sufficient:

def class_function_w_dict_argument(self, T_in, *, T1, T2, T3, T4, T5, **kwargs):

The * to explicitly make all the remaining arguments into keyword arguments is optional as well.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • In general I agree: don't use a dictionary just include the actual named parameters as keywords. However, what if you don't know the keywords? Such is the case when creating a generic function. The most recent example I have of this is [gym.reset](https://www.gymlibrary.ml/content/api/?highlight=reset#resetting) where environment specific options are taken as a dictionary argument. In this case, it makes sense for the generic function to take a dictionary, but when I override this function, I want to document all of the keys in the dictionary. – Kraigolas Aug 01 '22 at 21:37
  • @Kraigolas. If you really don't know the keywords, documenting them is not an issue. If you do, then **kwargs is designed to do exactly what you want – Mad Physicist Aug 01 '22 at 21:53
  • "If you really don't know the keywords, documenting them is not an issue." The person writing a generic function does not know the keywords, so they allow a dictionary for the person who does override the function. The person overriding the function knows the keywords, so they want to document them. – Kraigolas Aug 01 '22 at 21:58
  • 1
    @Kraigolas. Exactly, which is how **kwargs works – Mad Physicist Aug 01 '22 at 22:21
  • This doesn't appear to answer the question, which is how to document the given code; this seems to offer suggestions on how to rewrite the code and document _that_ rather than how to document the code that OP wrote. – TylerH Sep 01 '22 at 13:38
  • @TylerH. "Don't" is not always a good answer, but it is an answer. You wouldn't answer an XY problem by addressing the Y. – Mad Physicist Sep 01 '22 at 13:58
  • @MadPhysicist But you _aren't_ saying "don't"... you're assuming OP's design causes problems, but they didn't say anything about that in the question. NB - Cody and I have been discussing this question in chat, and the point is possible that I'm misunderstand what OP is asking here as I don't know Python, but it sounds like OP is asking how to write documentation (e.g. comments) that explain how to use the function he wrote – TylerH Sep 01 '22 at 14:30
  • Hence my concern here, it looks like instead of providing input on how OP wrote his documentation for his function, you're saying "well if you have design problems, you should write your function this way". ...OP's not asking about his function, OP's asking about his documentation. That's not really an XY problem just because you don't like how they wrote their function. – TylerH Sep 01 '22 at 14:31