*args & **kwargs
Overview
*args and **kwargs let a function accept a variable number of arguments. *args collects any extra positional arguments into a TUPLE: def log(*messages): for m in messages: print(m) log('start', 'processing', 'done') # messages = ('start', 'processing', 'done') **kwargs collects any extra keyword arguments into a DICT: def create_record(**fields): return fields create_record(name='alice', role='admin') # fields = {'name': 'alice', 'role': 'admin'} Both can be combined in one signature: def f(*args, **kwargs). The * and ** can also be used at the call site to UNPACK a sequence or dict into positional/keyword arguments: params = {'host': 'localhost', 'port': 5432} connect(**params) # same as connect(host='localhost', port=5432) Think of *args as a tuple-shaped funnel that catches extra positional values, and **kwargs as a dict-shaped funnel that catches extra keyword values. `*args` packs extra positionals into a tuple, `**kwargs` packs extra keywords into a dict -- consistently, regardless of the call site.
Wrapper functions, decorators, and logging helpers must accept any combination of arguments to forward them. *args/**kwargs are the tool for writing these adaptors without hard-coding every parameter.
Where used: FastAPI dependency injection wrappers, logging helper functions, decorator factories that forward arguments
Why learn this
- Writing decorator wrappers that forward any arguments to the wrapped function without listing every parameter
- Using **dict unpacking to pass config dicts as keyword arguments to FastAPI or database clients
Code walkthrough
def connect(host, port, **options):
print(f'connecting to {host}:{port}')
print(f'options: {options}')
creds = {'timeout': 30, 'ssl': True}
connect('localhost', 5432, **creds)
Focus: **creds at the call site unpacks the dict into keyword arguments; inside connect they are re-collected into the options dict — showing the unpack/collect round-trip.
Common mistakes
- Confusing *args at definition vs call site: In a def signature, *args COLLECTS extra positionals into a tuple. At the call site, *sequence UNPACKS a sequence into positional arguments. Same syntax, opposite direction.
- Mutating kwargs thinking it won't affect the caller: kwargs is a fresh dict created by Python, so mutating it is safe. But if you forward **kwargs and the receiving function mutates a mutable value inside, the original object is affected.
- Putting a regular parameter after *args: def f(*args, x) makes x keyword-only (it cannot be passed positionally). This is intentional in keyword-only params but surprises people who expect it to be positional.
Recall questions
- What type does *args collect extra positional arguments into?
- What type does **kwargs collect extra keyword arguments into?
- What does ** do at the CALL SITE (not in a function definition)?
- Why are *args and **kwargs essential for writing wrapper or decorator functions?
Understanding checks
What is printed by the function call?
<class 'tuple'> <class 'dict'>
`*args` collects positional arguments into a tuple, and `**kwargs` collects keyword arguments into a dict.
Why does this code throw a TypeError?
The `params` dictionary is being passed as a positional argument, not unpacked into keyword arguments.
To pass the dictionary as keyword arguments, you must use the `**` operator at the call site: `process_user('Alice', **params)`. Otherwise, `params` is assigned to a positional parameter. Since the function only takes one positional argument (`name`), Python complains that too many positional arguments were given.
Practice tasks
Flexible request logger
The `log_request` function currently only logs a single path string and a single predefined header. Modify the function signature and implementation so it can accept ANY number of path parts as positional arguments (`*args`), which should be joined by '/', and ANY number of headers as keyword arguments (`**kwargs`), which should be printed as 'key: value'.
Challenge
Generic function forwarder
Write timed_call(fn, *args, **kwargs) that calls fn(*args, **kwargs), prints 'called <fn.__name__>', and returns the result. Test it by forwarding calls to the built-in sorted() with a list and a key function.
Continue learning
Previous: Mutable vs immutable types