Documenting dictionaries passed as argument in docstrings

Viewed 1681

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
    """
2 Answers

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

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.

Related