Type annotations for *args and **kwargs

I'm trying out Python's type annotations with abstract base classes to write some interfaces. Is there a way to annotate the possible types of *args and **kwargs?

For example, how would one express that the sensible arguments to a function are either an int or two ints? type(args) gives Tuple so my guess was to annotate the type as Union[Tuple[int, int], Tuple[int]], but this doesn't work.

    from typing import Union, Tuple

    def foo(*args: Union[Tuple[int, int], Tuple[int]]):
        try:
            i, j = args
            return i + j
        except ValueError:
            assert len(args) == 1
            i = args[0]
            return i

    # ok
    print(foo((1,)))
    print(foo((1, 2)))
    # mypy does not like this
    print(foo(1))
    print(foo(1, 2))

Error messages from mypy:

    t.py: note: In function "foo":
    t.py:6: error: Unsupported operand types for + ("tuple" and "Union[Tuple[int, int], Tuple[int]]")
    t.py: note: At top level:
    t.py:12: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
    t.py:14: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
    t.py:15: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
    t.py:15: error: Argument 2 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"

It makes sense that mypy doesn't like this for the function call because it expects there to be a tuple in the call itself. The addition after unpacking also gives a typing error that I don't understand.

How does one annotate the sensible types for *args and **kwargs?

For variable positional arguments (*args) and variable keyword arguments (**kw) you only need to specify the expected value for one such argument.

From the Arbitrary argument lists and default argument values section of the Type Hints PEP:

Arbitrary argument lists can as well be type annotated, so that the definition:

>     def foo(*args: str, **kwds: int): ...
>

is acceptable and it means that, e.g., all of the following represent function calls with valid types of arguments:

>     foo('a', 'b', 'c')
>     foo(x=1, y=2)
>     foo('', z=0)
>

So you'd want to specify your method like this:

    def foo(*args: int):

However, if your function can only accept either one or two integer values, you should not use *args at all, use one explicit positional argument and a second keyword argument:

    def foo(first: int, second: Optional[int] = None):

Now your function is actually limited to one or two arguments, and both must be integers if specified. *args always means 0 or more, and can't be limited by type hints to a more specific range.

From: stackoverflow.com/q/37031928

Back to homepage or read more recommendations: