Variadic Arguments (*args and **kwargs)
Variadic Arguments (*args and **kwargs)
After reading this chapter, you will master variadic parameters, construct flexible APIs using *args and **kwargs, and write transparent argument forwarding wrappers.
Mental model
Variadic parameters allow a function to accept an arbitrary number of positional or keyword arguments: - *args: Collects extra positional arguments into a tuple. - **kwargs: Collects extra keyword arguments into a dict.
Call Site:
send_payload("POST", 200, "fast", verbose=True, timeout=10)
│ │ │ │ │
▼ └─────────┼──────────┼────────────┼────────────────┐
method: "POST" ▼ ▼ ▼ ▼
┌───────────────┐ ┌─────────────────────────┐
│ args: (tuple) │ │ kwargs: (dict) │
│ (200, "fast") │ │ {"verbose": True, ...} │
└───────────────┘ └─────────────────────────┘
At call sites, the operators can be reversed: * unpacks an iterable into positional arguments, and ** unpacks a mapping into keyword arguments.
Minimal example
Save as variadic_demo.py:
# variadic_demo.py
def format_log(level: str, *messages: str, **metadata: str | int) -> str:
joined_msg = " | ".join(messages)
meta_str = " ".join(f"[{k}={v}]" for k, v in metadata.items())
return f"[{level.upper()}] {joined_msg} {meta_str}".strip()
def main() -> None:
# Multiple positional messages, multiple keyword attributes
entry = format_log(
"info",
"Connection established",
"Handshake completed",
host="node-alpha",
port=9000,
region="us-west-2"
)
print(entry)
if __name__ == "__main__":
main()Run via uv run python variadic_demo.py:
[INFO] Connection established | Handshake completed [host=node-alpha] [port=9000] [region=us-west-2]
Worked examples
Case 1: Argument forwarding in decorator wrappers
When authoring wrappers or logging hooks, you must forward arguments cleanly to the wrapped target function:
# wrapper_forward.py
import time
from collections.abc import Callable
from typing import Any
def time_execution(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = func(*args, **kwargs) # Perfect forwarding with * and **
elapsed = time.perf_counter() - start
print(f"Function '{func.__name__}' executed in {elapsed * 1000:.3f}ms")
return result
def compute_heavy_task(base: int, power: int, tag: str = "computation") -> int:
return base ** power
if __name__ == "__main__":
out = time_execution(compute_heavy_task, 2, 500_000, tag="benchmark")
print(f"Task completed: result has {len(str(out))} digits")Run:
uv run python wrapper_forward.pyCase 2: Unpacking dictionaries and lists at call sites
You can dynamically assemble parameters in dictionaries and pass them into functions using **:
# dynamic_call.py
def connect_db(host: str, port: int, database: str, timeout: int = 30) -> None:
print(f"Connecting to {database} at {host}:{port} (timeout={timeout}s)")
if __name__ == "__main__":
config = {
"host": "postgresql.internal",
"port": 5432,
"database": "analytics",
"timeout": 60,
}
# Unpack config dictionary directly into function arguments
connect_db(**config)Run:
uv run python dynamic_call.pyPitfalls
Pitfall 1: Signature ordering violations
Python enforces strict parameter ordering: 1. Standard positional parameters 2. Positional-only boundary / 3. *args 4. Keyword-only parameters 5. **kwargs
Placing *args after **kwargs raises a SyntaxError.
Pitfall 2: Overusing **kwargs and destroying API readability
While **kwargs offers ultimate flexibility, overusing it hides what parameters a function actually accepts from documentation, autocomplete, and type checkers. Prefer explicit named parameters for core options.
Exercises
- Write a function
product(*numbers: float) -> floatthat computes the product of all passed numbers, returning1.0if no arguments are supplied. - Implement a function
merge_configs(*configs: dict) -> dictthat merges an arbitrary number of configuration dictionaries into a single dictionary in order. - Write a wrapper function
log_calls(func, *args, **kwargs)that prints all passed arguments before executingfunc.
Further reading
- Python Tutorial: Arbitrary Argument Lists.
- PEP 448: Additional Unpacking Generalizations.
- Fluent Python (2nd Edition): Chapter 7: Functions as First-Class Objects.