Type annotations for *args and **kwargs
--
Music by Eric Matyas
https://www.soundimage.org
Track title: Magic Ocean Looping
--
Chapters
00:00 Question
01:26 Accepted answer (Score 434)
02:46 Answer 2 (Score 53)
04:29 Answer 3 (Score 49)
06:50 Answer 4 (Score 28)
07:23 Thank you
--
Full question
https://stackoverflow.com/questions/3703...
Accepted answer links:
[Arbitrary argument lists and default argument values]: https://www.python.org/dev/peps/pep-0484...
Answer 2 links:
[@overload]: https://docs.python.org/3/library/typing...
Answer 3 links:
[added]: https://github.com/python/mypy/pull/1347...
[Mypy 0.981]: https://mypy-lang.blogspot.com/2022/09/m...
[GitHub issue]: https://github.com/python/mypy/issues/44...
--
Content licensed under CC BY-SA
https://meta.stackexchange.com/help/lice...
--
Tags
#python #typehinting #pythontyping
#avk47
ACCEPTED ANSWER
Score 530
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.
ANSWER 2
Score 117
2022 Update
The mypy team added support for Unpack, this is available since Mypy 0.981 or higher.
Attention! Although this feature is complete, Unpack[...] is still considered experimental, so you will need to use --enable-incomplete-features to enable it.
You can use this feature as follows:
from typing import TypedDict
from typing_extensions import Unpack
class RequestParams(TypedDict):
url: str
allow_redirects: bool
def request(**kwargs: Unpack[RequestParams]) -> None:
...
If you call the request function with the arguments defined in the TypedDict, you won't get any errors:
# OK
request(url="https://example.com", allow_redirects=True)
If you forget to pass an argument, mypy will let you know now 😊
# error: Missing named argument "allow_redirects" for "request" [call-arg]
request(url="https://example.com")
You can also make the fields non-required by adding total=False to the TypedDict:
class RequestParams(TypedDict, total=False):
url: str
allow_redirects: bool
# OK
request(url="https://example.com")
Alternatively, you can use the Required and NotRequired annotations to control whether a keyword argument is required or not:
from typing import TypedDict
from typing_extensions import Unpack, NotRequired
class RequestParams(TypedDict):
url: str
allow_redirects: NotRequired[bool]
def request(**kwargs: Unpack[RequestParams]) -> None:
...
# OK
request(url="https://example.com", allow_redirects=True)
Old answer below:
While you can annotate variadic arguments with a type, I don't find it very useful because it assumes that all arguments are of the same type.
The proper type annotation of *args and **kwargs that allows specifying each variadic argument separately is not supported by mypy yet. There is a proposal for adding an Expand helper on mypy_extensions module, it would work like this:
class Options(TypedDict):
timeout: int
alternative: str
on_error: Callable[[int], None]
on_timeout: Callable[[], None]
...
def fun(x: int, *, **options: Expand[Options]) -> None:
...
The GitHub issue was opened on January 2018 but it's still not closed. Note that while the issue is about **kwargs, the Expand syntax will likely be used for *args as well.
ANSWER 3
Score 55
The easiest way to do this -- without changing your function signature -- is using @overload
First, some background. You cannot annotate the type of *args as a whole, only the type of the items in args. So you can't say that *args is Tuple[int, int] you can only say that the type of each item within *args is int. That means that you can't put a limit on the length of *args or use a different type for each item.
To solve this you can consider changing the signature of your function to give it named arguments, each with their own type annotation, but if want (or need) to keep your function using *args, you can get mypy to work using @overload:
from typing import overload
@overload
def foo(arg1: int, arg2: int) -> int:
...
@overload
def foo(arg: int) -> int:
...
def foo(*args):
try:
i, j = args
return i + j
except ValueError:
assert len(args) == 1
i = args[0]
return i
print(foo(1))
print(foo(1, 2))
Note that you do not add @overload or type annotations to the actual implementation, which must come last.
You can also use this to vary the returned result in a way that makes explicit which argument types correspond with which return type. e.g.:
from typing import Tuple, overload
@overload
def foo(arg1: int, arg2: int) -> Tuple[int, int]:
...
@overload
def foo(arg: int) -> int:
...
def foo(*args):
try:
i, j = args
return j, i
except ValueError:
assert len(args) == 1
i = args[0]
return i
print(foo(1))
print(foo(1, 2))
ANSWER 4
Score 28
As a short addition to the previous answer, if you're trying to use mypy on Python 2 files and need to use comments to add types instead of annotations, you need to prefix the types for args and kwargs with * and ** respectively:
def foo(param, *args, **kwargs):
# type: (bool, *str, **int) -> None
pass
This is treated by mypy as being the same as the below, Python 3.5 version of foo:
def foo(param: bool, *args: str, **kwargs: int) -> None:
pass