*args, **kwargs and parameter kinds

Python · Practical · 12 min

Designing clear function signatures — and reading library ones — needs all the parameter kinds.

Beyond plain parameters, a Python signature can declare several kinds of parameter:

  • *args collects extra positional arguments into a tuple
  • **kwargs collects extra keyword arguments into a dict
  • parameters after * (or after *args) are keyword-only: callers must name them
  • parameters before / are positional-only: callers can't name them

The full order is def f(pos_only, /, normal, *args, kw_only, **kwargs).

Keyword-only parameters are a great design tool. def connect(host, *, timeout=10, retries=3) forces call sites to say timeout=5, which keeps them readable and lets you reorder or add options later without breaking anyone. Positional-only parameters do the reverse: they let you rename a parameter freely, and many built-ins use them (len(obj, /)).

The same stars unpack at the call site: f(*items) spreads a sequence into positional arguments, and f(**options) spreads a dict into keyword arguments. Together they enable the forwarding idiom, where a wrapper accepts *args, **kwargs and passes them straight through to another function — the foundation of decorators.

*args and **kwargs are conventional names; only the stars matter. Use them when a function genuinely accepts a variable number of things (like print) or forwards arguments. Don't use **kwargs just to avoid writing a signature: explicit parameters are self-documenting, checked by Python, and understood by your editor and type checker.

Example

def log(message, *values, level="INFO", **context):
    extra = " ".join(f"{k}={v}" for k, v in context.items())
    print(f"[{level}] {message}", *values, extra)

log("started")                         # [INFO] started
log("sum is", 3, 4, level="DEBUG", user="ana")
# [DEBUG] sum is 3 4 user=ana

def connect(host, port=5432, /, *, timeout=10):
    return f"{host}:{port} (timeout={timeout})"

print(connect("db", 6543, timeout=2))  # db:6543 (timeout=2)
# connect(host="db")       -> TypeError: positional-only
# connect("db", 1, 2)      -> TypeError: timeout is keyword-only

point = (3, 4)
opts = {"sep": " | ", "end": "!\n"}
print(*point, **opts)                  # 3 | 4!

def wrapper(*args, **kwargs):          # forward everything
    return connect(*args, **kwargs)
print(wrapper("api", timeout=1))       # api:5432 (timeout=1)

Remember

  • *args is a tuple of extra positionals, **kwargs a dict of extra keywords
  • Parameters after * are keyword-only; before / are positional-only
  • f(*seq, **mapping) unpacks at the call site

Watch out for

  • Hiding a function's real options behind **kwargs, so typos are silently ignored.

Check yourself

  1. Given `def f(a, *, b): ...`, which call is valid?
  2. What is `kwargs` inside `def g(**kwargs)` when called as `g(x=1, y=2)`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions