Explain Codes LogoExplain Codes Logo

Type annotations for *args and **kwargs

python
type-hints
function-overloading
typeddict
Anton ShumikhinbyAnton Shumikhin·Dec 27, 2024
TLDR

For type annotation of *args, use single-type Type or multi-type Union[Type1, Type2], and for **kwargs, use Dict[KeyType, ValueType]. For varied types apply Any.

def func(*args: int, **kwargs: str) -> None: pass # args are ints here, kwargs are strs; like soup without the soup

To transcend towards Python 3.10+ for granular control, embrace ParamSpec.

from typing import ParamSpec, Callable P = ParamSpec('P') # "P" for Pythonic, or pepperoni. Your choice. def wrapper(func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: return func(*args, **kwargs) # Aaron Burr, Sir - we're returning your function call

Making sense of protocols and ABCs

Wait, is your *args and **kwargs fulfill a certain protocol or an ABC (Abstract Base Classes) interface? Protocol and ABCs with type annotations are your saviors. They're like the cleaning crew, making your code sparkling clean and its intent more readable.

from typing import Protocol class SupportsClose(Protocol): def close(self) -> None: ... def close_all(*args: SupportsClose) -> None: for item in args: item.close() # We're not just closing doors, we're closing files too!

The magic of function overloading

@overload rounds up different combinations of *args wrapping them into various return types. It's the whisperer for your type checker, hinting about the varying sets of parameters your function is friendly with.

from typing import overload, Union @overload def process_values(*args: int) -> int: ... @overload def process_values(*args: str) -> str: ... def process_values(*args: Union[int, str]) -> Union[int, str]: # It's dinner time! We're serving sum soup or string spaghetti return sum(args) if all(isinstance(arg, int) for arg in args) else ','.join(args)

The power typing of *args and **kwargs with TypedDict and Unpack

Take your **kwargs typing to new heights using TypedDict for defining expected types. It's as if you added a turbo charger for your type checking:

from typing import TypedDict class Options(TypedDict): user: str retries: int def connect(**kwargs: Options) -> None: ... # Connecting... Keep your seat belts fastened until we've reached cruising altitude

In Mypy 0.981+ or higher, you'll find Unpack waiting for activation with the --enable-incomplete-features flag:

from typing_extensions import Required, NotRequired, Unpack class MyParams(TypedDict): a: Required[int] b: NotRequired[str] def my_func(*args: Unpack[MyParams]) -> None: ... # Functions are like joyful gatherings, we invite them via *.args

Conventions and obstacles with *args and **kwargs

Dealing with *args

Remember, *args is a ravenous creature - it takes an unlimited number of arguments. What’s that mean? You cannot limit the number of int inputs:

def sum_integers(*args: int) -> int: return sum(args) # *args, the mathematical Cookie Monster

Mixed bag of argument types

What if *args takes mixed types? Go with object and employ type narrowing through runtime checks to maintain peace:

def combine(*args: object) -> str: return " ".join(str(arg) for arg in args if isinstance(arg, (str, int, float))) # Join the party, args! Drinks (or rather, types) of your choice.

Enforcing signatures

As of Python 3.10, you can be the traffic warden for types with ParamSpec. It lets you declare types of *args and **kwargs:

from typing import ParamSpec P = ParamSpec('P') def repeat_call(func: Callable[P, None], times: int, *args: P.args, **kwargs: P.kwargs) -> None: for _ in range(times): func(*args, **kwargs) # "ring, ring" - it's your function calling again

Python 2 considerations

While Python 2 doesn’t natively support type hints, use type comments are your rescue flare:

def historical_func(*args, **kwargs): # type: (*int, **str) -> None pass # A walk down past language constructs