print('a') works. So does print('a', 'b', 'c', 'd') — same function, any number of arguments. How? print's signature doesn't list forty parameters; it collects whatever arrives. Today you learn the two stars that make that possible, and read framework code without flinching.
What you'll learn
What
*args collects any extra positional arguments into a tuple; **kwargs collects any extra keyword arguments into a dictionary. The same stars, used at a CALL site, do the reverse: unpack a sequence or dict into individual arguments.
Why
Variable-length signatures power the tools you already use — print, max, str.format — and the wrapper pattern (accept anything, pass it along) is how decorators, logging layers, and framework callbacks work. You'll read *args/**kwargs in every serious codebase this year.
Where it's used
Utility functions taking any number of inputs, wrapper/passthrough functions, plotting calls forwarding style options, and unpacking config dicts into API calls.
Where this runs in production
print(*objects, sep=' ', end='\n') — the signature you've called a thousand times is *args plus keyword defaults. max() and min() work the same way.
wandb-style loggers wrap training functions with def wrapper(*args, **kwargs) so any model's fit call can be intercepted, timed, and forwarded unchanged.
Chart helpers accept **kwargs and forward them to the underlying trace constructor — one wrapper supports every styling option without redeclaring fifty parameters.
def average(*values):
if len(values) == 0:
return 0.0
return sum(values) / len(values)
print(average(4, 8)) # 6.0
print(average(1, 2, 3, 4, 5)) # 3.0
print(average()) # 0.0One signature, any arity. Callers pass naked values — no list-building ceremony. The empty-call guard is the same discipline as every accumulator: decide what zero inputs means.
A office mailroom has named pigeonholes for known recipients (regular parameters), one open tray for any unaddressed mail (*args — it all piles in, in arrival order), and a box of labeled envelopes for anything addressed to names without pigeonholes (**kwargs — kept WITH their labels). Nothing is refused; everything lands somewhere predictable.
**kwargs collects extra keyword arguments into a dictionary: call f(color='red', width=2) and inside, kwargs is {'color': 'red', 'width': 2}. You'll study dictionaries deeply next module — for now: kwargs['color'] reads a value, 'color' in kwargs tests presence, and .get('color', 'black') reads with a fallback. That's enough to use the pattern.
def wrapper(*args, **kwargs): ...do something extra...; return real_function(*args, **kwargs) — collect EVERYTHING, forward EVERYTHING. The wrapper works for any function with any signature, which is why timing, logging, retry, and caching layers are all written this shape. When you meet decorators later, this is the entire trick.
A signature of (*args, **kwargs) tells readers NOTHING about what the function actually needs — no editor autocomplete, no loud arity errors, typos in option names silently vanish into kwargs. Use explicit parameters when you know the inputs; reach for stars only when genuine variability (any count, forwarding) is the point. A concrete tell: if the first line of your body is args[0], you wanted a named parameter.
log('save', 'ok', 200, user='ada', retry=True) hits def log(event, *details, **context). Click each node.
A checkout that totals any number of item prices.
prices is a tuple of whatever arrives. sum() doesn't care whether it gets 2 values or 20.
Your task
Build a scoreboard formatter. Define record_game(winner, *scores, **details) that returns a summary line: the winner's name, the number of rounds (len of scores), the total score, plus any extra details appended as key=value pairs. Produce the two outputs shown.
Mia won 3 rounds, 30 points Kai won 2 rounds, 24 points | venue=online, season=3
Write your solution in the editor on the right, then hit Run.
Inside def f(*args):, what TYPE is args?
def f(a, *rest): ... called as f(1, 2, 3, 4) — what is rest?
point = [3, 4] and def dist(x, y): ... — which call spreads the list into x and y?
What does this illustrate about **kwargs?
Define longest(*words) returning the longest string passed (first wins ties). Print longest('pandas', 'numpy', 'sklearn') and longest('a', 'bc'). Expected: sklearn bc
Which signature is INVALID?
You're given def send(to, subject, priority='normal'): return f'{to} <- {subject} [{priority}]'. Build msg = {'to': 'ops@acme.io', 'subject': 'disk alert', 'priority': 'high'} and call send using ** unpacking. Print the result. Expected: ops@acme.io <- disk alert [high]
What are *args and **kwargs, and what do the stars do at call sites?
In a signature, *args collects surplus positional arguments into a tuple and **kwargs collects surplus keyword arguments into a dict — the names are pure convention; the stars are the syntax. At a call site the stars invert: *sequence spreads its elements as positional arguments and **dict spreads its items as keyword arguments, with keys matching parameter names. So the same two symbols are 'pack' in a def and 'unpack' in a call. The canonical demonstration is the pass-through wrapper — def wrapper(*args, **kwargs): return f(*args, **kwargs) — which collects anything and forwards it unchanged, and is the mechanical basis of decorators, logging shims, and retry layers.
When is (*args, **kwargs) the RIGHT signature, and when is it a design smell?
Right: when variability is the point. Genuine any-count functions (sum-like utilities, print), forwarding wrappers that must fit every wrapped signature, and adapters passing options through to an underlying library (a chart helper forwarding **style to matplotlib). Smell: when the function actually requires specific inputs but hides them — a body starting with args[0], args[1] wanted named parameters; readers get no autocomplete, no arity errors, and misspelled options silently vanish into kwargs instead of raising TypeError. My review heuristic: stars for forwarding and true variadics, explicit names for everything the function itself consumes — and when both occur, consume by name and forward the rest.
Explain the full parameter ordering rule and why Python enforces it.
The canonical order is: regular positional parameters, then *args, then keyword-default parameters, then **kwargs — e.g. def report(title, *rows, sep=', ', **options). The order exists because binding must be unambiguous: positionals fill named slots left to right, *args must mark where 'the rest of the positionals' begins, anything after *args can only arrive by name (which is why parameters there are effectively keyword-only), and **kwargs sweeps up remaining names so it must be last. A worthwhile aside: a bare * in a signature — def f(data, *, strict=False) — is the same mechanism used purely to FORCE keyword-only options, which is how modern libraries prevent unreadable positional flag calls.
Common Mistakes to Avoid
1) Wrong zone order in a signature — **kwargs before *args is a SyntaxError. 2) Indexing args[0]/args[1] for required inputs — declare real parameters instead. 3) Forgetting the stars when forwarding: f(args, kwargs) passes a tuple and a dict as TWO arguments; f(*args, **kwargs) forwards the originals. 4) Expecting typo protection from **kwargs — unknown names are swallowed silently. 5) Passing a list where naked values are expected: average([1,2,3]) puts one list in the tuple; average(1,2,3) or average(*nums) is what you meant. 6) Mutating the kwargs dict then forwarding it, surprising the inner function.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'Quiz me: for each of these six calls, what lands in args and kwargs?' • 'Show me a wrapper that logs, then forwards, any function call.' • 'When should I use explicit parameters instead of stars? Critique this signature.' • 'Demonstrate * and ** unpacking with a config-dict example.' • 'Interview mode: ask me the parameter ordering rules and grade my answer.'
Glossary
*args — collects surplus positional arguments into a tuple (name conventional). **kwargs — collects surplus keyword arguments into a dict. Packing — the collecting behavior in a signature. Unpacking — the spreading behavior at a call site (*sequence, **dict). Variadic function — one accepting a variable number of arguments. Pass-through wrapper — a function that collects everything and forwards it to another. Keyword-only parameter — declared after *args (or a bare *), passable only by name. .items() — dict method yielding (key, value) pairs. func.__name__ — a function object's own name string.
Recommended Resources
• Docs: 'Arbitrary Argument Lists' and 'Unpacking Argument Lists' in the official Python tutorial. • Read: the signature of print() in the built-ins docs — *objects, sep, end, and you now know why it reads that way. • Practice: write describe(*nums, **options) returning min/max/mean with an optional decimals= option, then call it by unpacking a list and a dict. • Next in DSM: some functions are so small they don't deserve a name — Lambda Functions covers Python's one-expression anonymous functions and where they genuinely belong.
Recap
✓ *args packs surplus positionals into a tuple; **kwargs packs surplus keywords into a dict. ✓ Signature zones in fixed order: regular, *args, keyword defaults, **kwargs. ✓ At call sites the stars invert: *sequence and **dict spread data into arguments. ✓ The pass-through wrapper — collect everything, forward everything — powers logging, timing, and (later) decorators. ✓ Stars trade safety for flexibility: no arity errors, silent option typos — use real parameters when inputs are known. ✓ Parameters after *args are keyword-only, a feature libraries use deliberately. Next up: Lambda Functions. You've built full functions with def — next, the one-line anonymous kind you'll pass to sorted(), and the judgment call of when NOT to use them.
Run your code to see the output here.