*args & **kwargs
Sometimes you don't know in advance how many arguments a function will need to accept. Python solves this with two special parameters: *args for a variable number of positional arguments, and **kwargs for a variable number of keyword arguments. The names args and kwargs are just convention — the * and ** are what actually matter.
`*args`: Variable Positional Arguments
A parameter prefixed with a single * collects any extra positional arguments into a tuple.
def total(*args):
print(f"Received: {args}")
return sum(args)
print(total(1, 2, 3))
print(total(10, 20, 30, 40))
print(total())Received: (1, 2, 3) 6 Received: (10, 20, 30, 40) 100 Received: () 0
`**kwargs`: Variable Keyword Arguments
A parameter prefixed with ** collects any extra keyword arguments into a dictionary, where the keys are the argument names as strings.
def print_profile(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_profile(name="Ada", role="Engineer", years=12)name: Ada role: Engineer years: 12
Combining Regular Parameters with `*args`/`**kwargs`
Regular parameters, *args, and **kwargs can all appear in the same signature. Python requires a strict order: normal parameters first, then *args, then keyword-only parameters, then **kwargs last.
def build_report(title, *sections, author="Anonymous", **metadata):
print(f"Report: {title} (by {author})")
print("Sections:", sections)
print("Metadata:", metadata)
build_report(
"Q3 Sales",
"Summary",
"Charts",
"Appendix",
author="Ada",
version=2,
confidential=True,
)Report: Q3 Sales (by Ada)
Sections: ('Summary', 'Charts', 'Appendix')
Metadata: {'version': 2, 'confidential': True}Unpacking When Calling a Function
The * and ** symbols also work in reverse — at the call site, they unpack an existing list/tuple or dictionary into separate positional or keyword arguments.
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
print(add(*numbers)) # unpacks to add(1, 2, 3)
data = {"a": 10, "b": 20, "c": 30}
print(add(**data)) # unpacks to add(a=10, b=20, c=30)6 60
This is extremely handy for forwarding arguments from one function to another without knowing their exact shape.
def call_and_log(func, *args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"Result: {result}")
return result
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
call_and_log(greet, "Ada", greeting="Hi")Calling greet with args=('Ada',), kwargs={'greeting': 'Hi'}
Result: Hi, Ada!Real-World Example: A Flexible Logging Wrapper
Here's a small utility that logs every call to any function, regardless of its signature, by capturing arguments with *args/**kwargs and forwarding them.
import time
def timed_call(func, *args, **kwargs):
"""Run func with the given arguments and log how long it took."""
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__}(...) took {elapsed:.6f}s -> {result!r}")
return result
def power(base, exponent=2):
return base ** exponent
timed_call(power, 2, exponent=16)
timed_call(sum, [1, 2, 3, 4, 5])power(...) took 0.000002s -> 65536 sum(...) took 0.000001s -> 15
Quick Reference
Syntax | Where used | Collects into |
|---|---|---|
| function signature | A |
| function signature | A |
| function call | Unpacks into positional arguments |
| function call | Unpacks into keyword arguments |
The names
argsandkwargsare conventions —*valuesand**optionswork identically.A function can accept both
*argsand**kwargsat once for maximum flexibility.Unpacking with
*/**at a call site works with any function, not just ones defined with*args/**kwargs.