Python: Typing a function with *args and **kwargs

Updated: February 17, 2024 By: Guest Contributor Post a comment

Introduction

Python’s dynamic nature allows for flexible function definitions – among these, the use of *args and **kwargs stands out for enabling functions to accept an arbitrary number of positional and keyword arguments, respectively. With the introduction and growing adoption of type hints, there’s a parallel need for developers to articulate the types of these parameters within their code to maintain clarity, robustness, and developer tooling support. This tutorial explores how to utilize type hints effectively with functions that leverage *args and **kwargs.

Understanding *args and **kwargs

Before diving into typing, let’s establish what *args and **kwargs are. *args is used in function definitions to pass a variable number of non-keyword arguments which are accessed like a tuple. Similarly, **kwargs allows for passing a variable number of keyword arguments, which are accessible as a dictionary.

def example_function(*args, **kwargs):
    for arg in args:
        print(arg)
    for key, value in kwargs.items():
        print(f"{key}: {value}")

Adding Type Hints to *args and **kwargs

To start typing *args and **kwargs, one must first understand the basics of type hints in Python. Type hints are a formal solution to annotate variables, function parameters, and return values with their expected types. For *args, we use the tuple type hint, and for **kwargs, the dict or more specific typing.Mapping forms are used.

from typing import Any, Tuple, Mapping
def typed_example(*args: Tuple[Any, ...], **kwargs: Mapping[str, Any]):
    pass

This example defines *args as a tuple of any type and **kwargs as a mapping from strings to any type, serving most generic use-cases.

Specifying More Precise Types

While Any works for demonstrating the concept, most scenarios benefit from more precise type definitions. For intances, if you’re developing a function that expects all positional arguments to be integers and all keyword arguments to be floats, you can specify this directly.

from typing import Tuple, Dict
def precise_typed_example(*args: Tuple[int, ...], **kwargs: Dict[str, float]):
    pass

Utilizing Type Hints with Varied Parameter Types

Sometimes, parameters accept a mix of types. Python’s Union type from the typing module lets you define parameters that can be one of several types. With Python 3.10 onwards, you can also use the | operator as shorthand for Union.

from typing import Tuple, Dict, Union
def mixed_type_args(*args: Tuple[Union[int, str], ...], **kwargs: Dict[str, Union[int, str]]):
    pass

This function can accept any combination of integers and strings in both *args and **kwargs, showcasing a more complex type-hinting scenario.

Forward References and the typing.ForwardRef

There may be cases when the type you want to hint at isn’t defined yet – perhaps because of circular dependencies or simply because the definition comes later in the code. Python provides the ForwardRef to handle such scenarios, which can be particularly useful with **kwargs.

from typing import ForwardRef, Tuple
def future_typed_example(*args: Tuple["CustomClass", ...]):
    # Use ForwardRef for CustomClass as it's not defined yet
    pass

This method anticipates the use of a custom class not yet defined and demonstrates anticipation in typing.

Practical Application: Decorators with Typed *args and **kwargs

Decorators are a common use case for functions accepting *args and **kwargs, and they can significantly benefit from clear type hints. Let’s illustrate this with an example of a logging decorator that prints the types of all arguments received.

from typing import Callable, Any, Tuple, Dict
def type_logging_decorator(f: Callable[..., Any]):
    def wrapped(*args: Tuple[Any, ...], **kwargs: Dict[str, Any]):
        arg_types = [type(arg).__name__ for arg in args]
        kwarg_types = {k: type(v).__name__ for k, v in kwargs.items()}
        print(f"Function {f.__name__} called with arguments types: {arg_types} and keyword arguments types: {kwarg_types}")
        return f(*args, **kwargs)
    return wrapped

This decorator not only uses type hints for *args and **kwargs but also illustrates their practical application, enhancing both readability and maintainability of code.

Conclusion

Typing functions with *args and **kwargs not only clarifies their intended use but can also enhance code analysis tools, making your code more robust and understandable. As Python’s type hinting system continues to evolve, leveraging these capabilities in your dynamic function signatures will greatly contribute to the clarity and quality of your codebase.