Collect and Unpack Function Arguments
Distinguish variable positional arguments, named metadata, and a keyword-only option in one function.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Collect and Unpack Function Arguments
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
The single and double stars in Python function calls can look like punctuation you have to memorize. They become much clearer when we follow where the values go. Imagine we have several scores already stored in a list. A function accepts any number of positional scores with *scores. Calling it with the list itself does not give it three scores; it gives it one positional argument whose value is a list. If the function then tries sum(scores), Python attempts to add that list to integer zero and fails. We will reproduce that error, inspect what a function actually collects, and then fix the call by unpacking the list.
- 00:40
We will also add a named scale option and arbitrary metadata, showing why the order of parameters matters. The companion lesson has a checked report-building task and examples you can run in its browser console. By the end, each star should answer a concrete question: are we collecting values into a function, or expanding values at the call site? The printed tuple contains one element, and that element is the entire list. The trailing comma in the tuple display is Python's usual notation for a one-item tuple. The function signature did exactly what it promises: *scores collected all positional arguments.
- 01:21
We supplied one positional argument. The TypeError is not a signal that sum is broken; it is evidence that the call shape differs from the function's expected numeric arguments. We could rewrite total to accept one list parameter, and that would be valid if the function's public interface were meant to receive a list. Here we want flexible positional scores, so the caller must expand values. This distinction is important for generic wrappers and other APIs that receive data in a container but call a function that expects separate arguments. Before fixing the call, look at a second issue: a report may need a scale option and free-form labels, and those should not be mistaken for more scores.
- 02:06
The required name is bound first. The two extra positional numbers go into a tuple called scores. Scale appears after *scores in the signature, which makes it keyword-only; the caller names it as scale=2. Any remaining named arguments enter the tags dictionary. Team is metadata, not another score. This separation lets the function compute a total while keeping labels available for the report. The parameter names scores and tags are descriptive choices; Python reacts to the star syntax, not to those exact words. At the call site, the star symbols reverse the direction: *values expands an iterable into positional arguments, and **metadata expands a mapping into named arguments.
- 02:53
That is why wrappers use a pair such as func(*args, **kwargs) to forward a call. Watch for a common trap: if you pass the scale number positionally after scores, it is just one more score. The keyword-only parameter keeps the intended role explicit. The single star turns the three list elements into three positional score arguments. The double star turns the mapping entry into a named team argument. The explicit scale remains separate from both. Sum gives ten, and scale two produces a total of twenty. The returned dictionary provides one place for the name, total, and tags.
- 03:33
There is no reason to mutate the input list or metadata mapping. In the website exercise, the starter already has the right signature but returns a placeholder total of zero. Complete the total expression, then run the checker with zero scores, negative scores, several tag keys, and a call that unpacks saved values. Those cases test the behavior of the interface rather than just the syntax. A stable required parameter such as name should usually stay explicit. Using **tags for optional labels is convenient, but using it for every important option can hide misspellings.
- 04:13
The first call has no positional scores, so scores is an empty tuple and the total is zero. The second call has negative and positive values, then applies a scale of three; group is a tag and scale is not. The third call proves that a stored tuple and dictionary can be expanded at the call site. These cases also explain why the interface is useful: callers may supply as many scores as they have and only the metadata fields they need. There is another boundary worth knowing before you use double-star unpacking with real data. If a caller supplies team='red' explicitly and also expands a mapping containing team='blue', Python raises TypeError for the duplicate keyword.
- 04:59
That is preferable to silently guessing which label wins. Merge or validate the mapping before the call if duplicate keys are possible. Likewise, avoid accepting every spelling as a free-form tag when a field controls important behavior. When debugging a starred call, print the collected tuple and dictionary once, as we did in the diagnosis section. It quickly reveals whether you sent one container or its elements. Then remove the diagnostic print when the behavior is clear. Finally, remember that flexibility creates responsibility: document what numbers are allowed, make critical options explicit, and test empty input.
- 05:40
A short signature with stars can be clear when each group of arguments has a distinct job.
Understand the concept
A function can collect extra positional arguments in a tuple by writing *scores in its signature. Extra named arguments can be collected into a dictionary with **tags. These are conventional names, not magic identifiers: the single and double stars determine the behavior. A call such as build_report('Ada', 2, 3, team='red') gives scores the tuple (2, 3) and tags the dictionary {'team': 'red'}.
A parameter after *scores, such as scale=1, is keyword-only. The caller must write scale=2 if they want to change it. That makes a configuration option visible at the call site and keeps it out of the collected scores. Place **tags last; otherwise the function signature is invalid. If a caller supplies the same named argument twice through explicit syntax and **mapping, Python raises TypeError.
The stars also work at the call site. *values expands an iterable into positional arguments, and **metadata expands a mapping into keyword arguments. This is not the same as passing values and metadata as two ordinary objects. To forward a call unchanged, use func(*args, **kwargs), as a decorator wrapper does. Still write explicit parameters for stable required inputs; unrestricted kwargs can hide misspellings if every unknown name is silently accepted.
The exercise builds a report from one required name, any number of numeric scores, an optional scale, and arbitrary metadata. Return a new dictionary with the total and a copy of the metadata. A call with no scores has total zero. The checks ensure scale is applied to the numeric total and is not accidentally included among the tags.
- *args collects a tuple
- **kwargs collects a dictionary
- A parameter after *args is keyword-only
- * and ** at a call site unpack values
See it step by step
Read the code, predict the output, then compare it with the result.
01. Collect extra positional values
def total(*numbers):
print(numbers)
return sum(numbers)
print(total(2, 3, 5))
print(total())(2, 3, 5)
10
()
0The function receives a tuple of all supplied positional numbers, including an empty tuple when there are none.
02. Keep a keyword-only setting separate
def describe(name, *scores, scale=1, **tags):
return name, sum(scores) * scale, tags
print(describe('Ada', 2, 3, scale=2, team='red'))('Ada', 10, {'team': 'red'})scale is bound to its named parameter; team is collected in tags.
03. Unpack at the call site
def pair(name, score, *, group):
return f'{name}: {score} ({group})'
values = ('Bo', 8)
options = {'group': 'A'}
print(pair(*values, **options))Bo: 8 (A)The single star supplies two positional values; the double star supplies the named group argument.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Collect optional positional values
A single star in a function signature collects extra positional arguments into a tuple. This lets a function accept any number of scores while keeping the required name explicit. With no scores, the tuple is empty and sum returns zero. That behavior is useful, but it must be part of the function's contract rather than an accidental result.
The name args is conventional, not required. We call the parameter scores to describe what those values mean. Passing a list without a star is different from passing its individual elements: the tuple would contain one list, and sum would try to add that list to an integer. Run this example, then try a no-score call and predict the tuple shown.
def total(*scores):
print(scores)
return sum(scores)
print(total(2, 3, 5))
print(total())(2, 3, 5)
10
()
0- Three positional values become a three-item tuple.
- sum totals the tuple.
- No extra values produce an empty tuple and total zero.
Separate named configuration from free-form metadata
A parameter after *scores is keyword-only, so scale must be supplied with its name. That avoids mistaking a scaling factor for another score. A double-star parameter at the end collects remaining named arguments into a dictionary. In the example, team enters tags, while scale binds to its explicit parameter and never appears in tags.
Allowing arbitrary tags is useful for optional labels, but it also means a misspelled name might be accepted silently. Keep important options explicit, as we do with scale. The task asks for a report dictionary with name, scaled total, and metadata. Test a call without scores, a call with several scores, and a call with multiple tag keys so every part of the signature has a visible role.
def describe(name, *scores, scale=1, **tags):
return name, sum(scores) * scale, tags
print(describe('Ada', 2, 3, scale=2, team='red'))
print(describe('Bo', team='blue'))('Ada', 10, {'team': 'red'})
('Bo', 0, {'team': 'blue'})- scores receives only the positional numbers.
- scale is named and doubles the total.
- All other named fields become tags.
Unpack a call and build a report
The same star symbols have a complementary use at the call site. A single star expands an iterable into positional arguments; a double star expands a mapping into named arguments. This is how generic wrappers forward calls, but it also makes a small data-driven call readable when values are already stored in a tuple and options in a dictionary.
The finished function returns a fresh report. It does not need to mutate the caller's metadata mapping because Python constructs the keyword-argument dictionary for the call. The checker verifies that scale is not included in tags and that an unpacked call behaves like a direct call. If you remove the stars at the call site, the function receives different types and the result no longer satisfies the contract.
def build_report(name, *scores, scale=1, **tags):
return {'name': name, 'total': sum(scores) * scale, 'tags': dict(tags)}
values = (2, 3)
metadata = {'team': 'red'}
print(build_report('Ada', *values, scale=2, **metadata)){'name': 'Ada', 'total': 10, 'tags': {'team': 'red'}}- The tuple expands into two score arguments.
- The mapping expands into the named team argument.
- The explicit scale is applied to the sum and stays outside tags.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Use sum(scores) * scale for total. Return a dictionary with name, total, and dict(tags). The signature already separates scale from **tags.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Passing a list as one positional score
build_report('Ada', [2, 3])build_report('Ada', *[2, 3])What happens: scores contains one list; sum(scores) tries to add a list to zero.
Use * at the call site when each element should become a separate positional argument.
Treating a keyword-only option as positional
describe('Ada', 2, 3, 2) # last 2 is scaledescribe('Ada', 2, 3, scale=2)What happens: All three numbers enter scores; scale remains its default.
An option after *scores must be named explicitly.