Python Type Hints - *args and **kwargs

A typographer’s governor (apparently).

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 ints, 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 variable args is deduced as Tuple[str, ...] and the type of variable kwds is Dict[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!

Fin

I’d like to have an argument, please.

—Adam


If your Django project’s long test runs bore you, I wrote a book that can help.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: ,