*args and **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') `**kwargs` collects any extra keyword arguments into a dictionary: def create_record(**fields): return fields create_record(name='alice', role='admin') Both can be combined in one signature like `def f(*args, **kwargs)`. The `*` and `**` operators can also be used at the call site to unpack a sequence or dict into positional or keyword arguments: params = {'host': 'localhost', 'port': 5432} connect(**params) 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. They pack extra arguments consistently, regardless of the call site. Watch out for parameter order when defining functions. A regular parameter placed after `*args` automatically becomes a keyword-only argument: def create_user(*roles, status): pass create_user('admin', 'editor', status='active') create_user('admin', 'editor', 'active') # TypeError: create_user() missing 1 required keyword-only argument: 'status' This behavior is useful but often catches beginners by surprise (covered fully in keyword-positional-params).
Wrapper functions, decorators, and logging helpers must accept any combination of arguments to forward them. `*args` and `**kwargs` are the tools 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 dictionaries 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 dictionary 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 parameters 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?
- What happens if you define a regular parameter after `*args`, like `def f(*args, x):`?
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: Defining functions and return values
Previous: Mutable vs immutable objects
Next: Keyword-only and positional-only parameters
Next: First-class functions: passing and returning functions