![A typographer’s governor (apparently). A typographer’s governor (apparently).](http://adamj.eu/tech/assets/2021-05-11-governor.jpg)
When I started writing type hints, I was a little confused about what to do with Python’s variable argument operators, *
and **
(often called *args
and **kwargs
). Here’s what I figured out.
Recall that the *
operator captures variable positional arguments in a tuple
, and **
captures variable keyword arguments in a dict
. For example, take this function:
def variable(*args, **kwargs): ...
In the function body, args
will be a tuple
, and kwargs
a dict with string keys.
When adding type hints, it seems natural to try declare the full types of args
and kwargs
. If we wanted all our values to be int
s, we might try:
def variable(*args: tuple[int, ...], **kwargs: dict[str, int]) -> None: ...
(The ...
in the tuple definition makes it a tuple of any length.)
But this is incorrect. We can check by adding a call:
variable(1, 2, 3, a=4, b=5, c=6)
Running Mypy on the file, it finds a problem with every argument(!):
$ mypy example.py example.py:5: error: Argument 1 to "variable" has incompatible type "int"; expected "Tuple[int, ...]" example.py:5: error: Argument 2 to "variable" has incompatible type "int"; expected "Tuple[int, ...]" example.py:5: error: Argument 3 to "variable" has incompatible type "int"; expected "Tuple[int, ...]" example.py:5: error: Argument "a" to "variable" has incompatible type "int"; expected "Dict[str, int]" example.py:5: error: Argument "b" to "variable" has incompatible type "int"; expected "Dict[str, int]" example.py:5: error: Argument "c" to "variable" has incompatible type "int"; expected "Dict[str, int]" Found 6 errors in 1 file (checked 1 source file)
Uh oh! What’s the right way then?
*
always binds to a tuple
, and **
always binds to a dict with string keys. Because of this restriction, type hints only need you to define the types of the contained arguments. The type checker automatically adds the tuple[_, ...]
and dict[str, _]
container types.
The Python Enhancement Proposal (PEP) that introduced type hints, PEP 484, specified this rule:
Arbitrary argument lists can as well be type annotated, so that the definition:
def foo(*args: str, **kwds: int): ...
is acceptable… In the body of function
foo
, the type of variableargs
is deduced asTuple[str, ...]
and the type of variablekwds
isDict[str, int]
.
So, we can correctly type our function as:
def variable(*args: int, **kwargs: int) -> None: ...
This then passes type checks:
$ mypy example.py Success: no issues found in 1 source file
Yay!
Learn how to make your tests run quickly in my book Speed Up Your Django Tests.
One summary email a week, no spam, I pinky promise.
Related posts: