Annotating args and kwargs in Python

Redowan Delowar January 8, 2024
Source
While I tend to avoid args and kwargs in my function signatures, it's not always possible to do so without hurting API ergonomics. Especially when you need to write functions that call other helper functions with the same signature. Typing args and *kwargs has always been a pain since you couldn't annotate them precisely before. For example, if all the positional and keyword arguments of a function had the same type, you could do this: This implies that args is a tuple where all the elements are integers, and kwargs is a dictionary where the keys are strings and the values are booleans. On the flip side, you couldn't annotate args and *kwargs properly if the values of the positional and keyword arguments had different types. In those cases, you'd have to fall back to Any, which defeats the purpose. Consider this example: Here, the type checker sees each positional argument as a tuple of an integer and a string. Plus, it considers each keyword argument as a dictionary where the keys are strings and the values are either booleans or None. With the previous annotation, mypy will reject this: Instead, it'll accept the following: You probably wanted to represent the former while the type checker wants the latter. To annotate the second instance correctly, you'll need to leverage bits of [PEP-589], [PEP-646], [PEP-655], and [PEP-692]. We'll use Unpack and TypedDict from the typing module to achieve this. Here's how: TypedDict was introduced in Python 3.8 to allow you to annotate heterogeneous dictionaries. If all the values of a dictionary have the same type, you can simply use dict[str, T] to annotate it. However, TypedDict covers the case where all the keys of a dictionary are strings but the type of the values varies. The following example shows how you might annotate a heterogeneous dictionary: Unpack marks an object as having been unpacked. Using TypedDict with Unpack allows us to communicate with the type checker so that each positional and keyword argument isn't mistakenly assumed as a tuple and a dictionary respectively. While the type checker is satisfied when you pass the args and *kwargs as it'll complain if you don't pass all the keyword arguments: To make all of the keywords optional, you could turn off the total flag in the typed-dict definition: Or you could mark specific keywords as optional with typing.NotRequired: This will let you pass an incomplete set of optional keyword arguments without the type checker yelling at you. Fin! [pep-589]: https://peps.python.org/pep-0589/ [pep-646]: https://peps.python.org/pep-0646/ [pep-655]: https://peps.python.org/pep-0655/ [pep-692]: https://peps.python.org/pep-0692/

Discussion in the ATmosphere

Loading comments...