pythonannotationsargumentstypingkeyword-argument

What is the best practice to annotate the *args and **kwargs arguments of a wrapper function in Python 3.11?


Consider the following wrapper function:

import numpy as np
from matplotlib import pyplot as plt
from typing import Any

def plot_histogram(data: np.ndarray,
                   xlabel: str | None = None,
                   ylabel: str | None = None,
                   log: bool = False,
                   *args: Any,
                   **kwargs: Any) -> plt.Axes:
    _, ax = plt.subplots()
    if log:
        data = np.log10(data)
        hist, bins = np.histogram(data, bins='auto')
        hist = hist.astype(float)
        bins = 10 ** bins
        bin_widths = np.diff(bins)
        hist /= (bin_widths * np.sum(hist))
        ax.loglog(bins[:-1], hist, *args, **kwargs)
    else:
        hist, bins = np.histogram(data, bins='auto', density=True)
        ax.plot(bins[:-1], hist, *args, **kwargs)
    if xlabel:
        ax.set_xlabel(xlabel)
    if ylabel:
        ax.set_ylabel(ylabel)
    return ax

I have read Python's typing documentation as well as PEP 612 and 692 (although the latter concerns Python 3.12), but I'm still unsure about the best practice to annotate the *args and **kwargs arguments in my use case.

As shown in the code snippet, I have currently used typing.Any as the annotation for *args and **kwargs, but it doesn't seem to be a good practice.

Edit: This question is deemed a duplicate of another question. However, my question specifically concerns the annotation of a wrapper function which is only addressed in one of the answers. Although the mentioned answer is through, I have seen other solutions to this problem and I would like to consider different viewpoints for this particular use case. The only answer my question received before getting closed was one such example.


Solution

  • This heavily depends on what you expect *args and **kwargs to be in your application.

    You are using *args and **kwargs to call the matplotlib plot() function (loglog() is just a wrapper around plot()). Its source code looks like this:

    def plot(
        *args: float | ArrayLike | str,
        scalex: bool = True,
        scaley: bool = True,
        data=None,
        **kwargs,
    ) -> list[Line2D]:
        return gca().plot(
            *args,
            scalex=scalex,
            scaley=scaley,
            **({"data": data} if data is not None else {}),
            **kwargs,
        )
    

    I would suggest copying that typing using the union object | (introduced in 3.10) for *args with the possible types. Always rememeber that typing is optional in Python. While it is often very useful, an annotation of "Any" for keyword arguments does not really provide any information to the reader.