pythonpython-sphinxtype-hintingdocstringsphinx-napoleon

Is there a standard format for documenting function-type parameters in Sphinx using sphinx.ext.napoleon?


I am using Sphinx to document one of my projects and one of the classes takes in a function as a parameter in its __init__. Is there a standard way to document this function-type parameter? I am also using sphinx.ext.napoleon to use Google formatting for my docstrings.

Here's an example:

class ExampleClass:
    """An example class to demonstrate my question
    
    Args:
        func (what goes here?): Description of the parameter
    """

    def __init__(self, func):
        self.func = func

    def do_something(self, param):
        """An example method

        Args:
            param (str): Description of param
        
        Returns:
            bool: The return description
        """

        return self.func(param)

In my code, the parameter in question should take in one str and return a bool. Is there a standard way to document this when using Sphinx?


Solution

  • The simplest way to document the function parameter is using typing.Callable. By doing so the static type checker will verify that the signature (arguments and return type) of the passed function is compatible with the type hint.

    from typing import Callable
    
    class ExampleClassTwo:
        """An example class to demonstrate my question.
    
        Args:
            func (Callable[[str], bool]): Description of the parameter.
        """
        def __init__(self, func: Callable[[str], bool]):
    
            self.func = func
    

    However, this has one potential problem. If you look at the Python Data Model, in the sub-section titled "Callable types" under 3.2 The standard type hierarchy. You'll notice there are several possible types that are callables, and any callable with the specified signature would not cause a warning from the static type checker. For example, a class instance implementing the __call__() method would not cause a warning:

    def str_function(param: str) -> bool:
        pass
    
    class OneInstance:
    
        def __init__(self, param):
            pass
    
        def __call__(self, param: str) -> bool:
            pass
    
    one_instance = OneInstance("test instance")
    # neither of the below raise a static type check warning
    one = ExampleClassTwo(str_function)
    two = ExampleClassTwo(one_instance)
    

    You can type hint the param signature as shown above together with validating that param is of type FunctionType in the __init__ as you would validate other parameters at run-time, and raise a TypeError exception if the parameter is not a function.

    from typing import Callable
    from types import FunctionType
    
    class ExampleClassTwo:
        """An example class to demonstrate my question
    
        Args:
            func (Callable[[str], bool]): Description of the parameter
        Raises:
            TypeError: Argument `param` is not of type ``FunctionType``.
        """
    
        def __init__(self, func: Callable[[str], bool]):
    
            if type(func) in (FunctionType,):
                self.func = func
            else:
                raise TypeError("param must be initialized as being of ``FunctionType``.")
    

    It may be possible to combine both requirements Callable[[str], bool] and FunctionType as an intersection using structural subtyping, I have not yet tried that approach.

    Finally some examples are included that would cause the static type checker to raise warnings:

    def int_function(param: int) -> bool:
        pass
    
    
    class ExampleClass:
        """An example class to demonstrate my question
    
        Args:
            func (FunctionType): Description of the parameter
        """
    
        def __init__(self, func: FunctionType):
            self.func = func
    
    three = ExampleClass(str_function("test string"))  # Expected type 'FunctionType', got 'bool' instead
    four = ExampleClass(str_function)  # Expected type 'FunctionType', got '(param: str) -> bool' instead
    five = ExampleClass(type(str_function))  # No warning five.func is {type} <class 'function'>
    
    six = ExampleClassTwo(int_function(2))  # Expected type '(str) -> bool', got 'bool' instead
    seven = ExampleClassTwo(str_function("test string"))  # Expected type '(str) -> bool', got 'bool' instead
    eight = ExampleClassTwo(int_function)  # Expected type '(str) -> bool', got '(param: int) -> bool' instead
    nine = ExampleClassTwo(str_function)  # No warning
    

    enter image description here