Real-world functions rarely take a fixed number of arguments. Users pass lists, dicts, extra options, forwarded arguments from wrappers. Python's toolkit for that is *args (a tuple of positional extras), **kwargs (a dict of keyword extras), keyword-only parameters, positional-only parameters, and default values. Combined, they let you design signatures that are both flexible and precise.
def fn(a, b, *args, **kwargs): accepts any number of extra positionals as args and any extra keywords as kwargs. Inside the function they are just a tuple and a dict. You can unpack them back out when calling another function: other(*args, **kwargs). That is how decorators and wrappers pass arguments through without knowing what they are.
Keyword-only parameters (def fn(a, *, flag=True):) must be passed by name. That makes calls self-documenting and prevents positional mistakes. Positional-only parameters (def fn(a, /, b):) go the other way — they can't be passed by name — useful when the parameter name is an implementation detail you might rename. Together with defaults they give you fine-grained control over how callers invoke a function.
Unpacking at the call site is the mirror image: fn(*my_list, **my_dict) spreads a list into positional arguments and a dict into keyword arguments. That pattern makes it trivial to forward partially-built argument bundles or load call arguments from configuration.
*args, **kwargs in definitions and calls
Inside a function, args is a tuple and kwargs is a dict. Outside, * unpacks a sequence into positional args and ** unpacks a mapping into keyword args. Same symbol, two contexts.
Never mutate the incoming containers if callers might reuse them. Copy if you plan to change.
Keyword-only and positional-only
Anything after a bare * in the signature is keyword-only. Anything before a / is positional-only. Use keyword-only for booleans and rarely-changed options (clearer at call sites); positional-only for arguments like x, y that clearly belong in a particular slot.
Defaults only apply when the argument is omitted. Mutable defaults (def f(x=[])) are the most famous Python gotcha — use x=None and x = [] if x is None else x.
Argument-handling tools.
| Tool | Purpose |
|---|---|
*argssyntax | Capture extra positional args. |
**kwargssyntax | Capture extra keyword args. |
def f(*, flag=True)syntax | Keyword-only parameters (PEP 3102). |
def f(x, /, y)syntax | Positional-only parameters (PEP 570). |
f(*list, **dict)syntax | Unpack at a call site. |
inspect.signaturefunction | Inspect a function's parameters. |
functools.partialfunction | Pre-bind some arguments. |
calls referencedocs | Full grammar of calls. |
Creating Flexible Input Handling code example
The script shows *args/**kwargs, keyword-only flags, safe default handling, and forwarding through a wrapper.
# Lesson: Creating Flexible Input Handling
from functools import wraps
def summarize(*values, sep=", ", prefix="", uppercase=False):
"""Join any number of values into one string."""
text = sep.join(str(v) for v in values)
if uppercase:
text = text.upper()
return prefix + text
print(summarize("ana", "ben", "cai"))
print(summarize(1, 2, 3, sep=" + ", prefix="sum = "))
print(summarize("hi", "there", uppercase=True))
def style(text: str, /, *, bold: bool = False, color: str = "default") -> str:
"""Text is positional-only; options are keyword-only."""
return f"[{color}{'/bold' if bold else ''}] {text}"
print(style("hello"))
print(style("hi", bold=True, color="red"))
try:
style(text="nope") # positional-only can't be passed by name
except TypeError as err:
print("type error:", err)
# Safe mutable default
def collect(*items, into=None):
if into is None:
into = []
into.extend(items)
return into
a = collect(1, 2)
b = collect(9, 10)
print("a:", a, "| b:", b) # both independent lists
# Forwarding: decorator wraps any function
def trace(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"-> {func.__name__}({args}, {kwargs})")
return func(*args, **kwargs)
return wrapper
@trace
def add(a, b, *, scale=1):
return (a + b) * scale
print("add:", add(2, 3, scale=10))
# Unpacking at call site
args = (4, 5)
opts = {"scale": 2}
print("unpacked:", add(*args, **opts))
Reading top to bottom:
1) `*values, sep=` mixes arbitrary positionals with named options.
2) `/` makes `text` positional-only; `*,` makes `bold`/`color` keyword-only.
3) Mutable default pattern: `None` sentinel plus in-body assignment.
4) `*args`, `**kwargs` forwarding is the core of every decorator.
Accept either a list or multiple values.
def mean(*values):
if len(values) == 1 and isinstance(values[0], (list, tuple)):
values = values[0]
if not values:
raise ValueError("no values")
return sum(values) / len(values)
print(mean(1, 2, 3)) # 2.0
print(mean([10, 20, 30])) # 20.0
Invariants of a flexible signature.
def f(*a, **k):
return a, k
assert f(1, 2, x=3) == ((1, 2), {"x": 3})
args, kw = (1, 2), {"x": 3}
assert f(*args, **kw) == ((1, 2), {"x": 3})
Running prints:
ana, ben, cai
sum = 1 + 2 + 3
HI, THERE
[default] hello
[red/bold] hi
type error: style() got some positional-only arguments passed as keyword arguments: 'text'
a: [1, 2] | b: [9, 10]
-> add((2, 3), {'scale': 10})
add: 50
-> add((4, 5), {'scale': 2})
unpacked: 18