Difficulty: Intermediate
Explain *args and kwargs. How and when do you use them?
*args collects extra positional arguments into a tuple. kwargs collects extra keyword arguments into a dictionary.
Order in function signature: regular args, *args, keyword-only args, kwargs.
Common use cases: - Writing flexible APIs that accept variable arguments - Forwarding arguments to another function (decorators, wrappers) - Creating functions that work with any number of inputs
The * and operators also work for unpacking in function calls and assignments.
def flexible(*args, kwargs):
print(f"Positional: {args}") # tuple
print(f"Keyword: {kwargs}") # dict
flexible(1, 2, 3, name='Alice', age=30)
# Real-world: logging wrapper
def log_call(func):
def wrapper(*args, kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
result = func(*args, kwargs)
print(f"Returned: {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
add(3, 5)
add(a=10, b=20)
*args captures positional arguments as a tuple, kwargs captures keyword arguments as a dict. Together they let you forward any arguments to the wrapped function.
# * unpacking in function calls
def add(a, b, c):
return a + b + c
nums = [1, 2, 3]
print(add(*nums)) # 6 - unpacks list into positional args
config = {'a': 10, 'b': 20, 'c': 30}
print(add(config)) # 60 - unpacks dict into keyword args
# * unpacking in assignments (Python 3)
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
head, *tail = [1, 2, 3]
print(head) # 1
print(tail) # [2, 3]
# * unpacking for merging
list1 = [1, 2, 3]
list2 = [4, 5, 6]
merged = [*list1, *list2, 7]
print(merged) # [1, 2, 3, 4, 5, 6, 7]
dict1 = {'a': 1}
dict2 = {'b': 2}
merged = {dict1, dict2, 'c': 3}
print(merged) # {'a': 1, 'b': 2, 'c': 3}
The * and operators unpack iterables and dicts respectively. In assignments, *variable captures remaining items into a list.
# Full argument order:
# def func(positional, /, normal, *, keyword_only, kwargs)
def api_call(url, /, method='GET', *, headers=None, timeout=30):
"""url is positional-only, headers and timeout are keyword-only."""
print(f"{method} {url}")
print(f"Headers: {headers}, Timeout: {timeout}")
api_call('https://api.example.com') # OK
api_call('https://api.example.com', 'POST', headers={'Auth': 'token'})
# api_call(url='...') # Error: url is positional-only
# api_call('...', 'GET', {'Auth': 'token'}) # Error: headers is keyword-only
# Forcing keyword-only with bare *
def safe_divide(a, b, *, raise_on_zero=False):
if b == 0:
if raise_on_zero:
raise ZeroDivisionError
return None
return a / b
print(safe_divide(10, 0)) # None
print(safe_divide(10, 0, raise_on_zero=True)) # Raises error
Arguments after * are keyword-only: they must be passed by name. Arguments before / are positional-only. This creates clear, self-documenting APIs.
*args, kwargs, Unpacking, Argument Forwarding, Variadic Functions