Exercises — ` - args` and ` - kwargs` — flexible argument passing
Quick refresher of the only rule you truly need:

Level 1 — Recognition
Here you only decide what type a name holds and which box a value lands in.
Exercise 1.1
State the exact value and Python type of args.
def f(*args):
return args
print(f(7, 8, 9))Recall Solution 1.1
The star in the definition collects all positional arguments into one tuple. Three values come in, so:
- Output:
(7, 8, 9) - Type: a tuple (round brackets, immutable).
What we did: read the star as "gather leftovers." Why: there are no named parameters before
*args, so every value is a leftover.
Exercise 1.2
What is kwargs here — value and type?
def g(**kwargs):
return kwargs
print(g(x=1, y=2))Recall Solution 1.2
** collects key=value pairs into a dict:
- Output:
{'x': 1, 'y': 2} - Type: a dict (curly braces, key→value).
Exercise 1.3
Which of these are legal function headers? Mark each.
# A
def a(*args, **kwargs): pass
# B
def b(**kwargs, *args): pass
# C
def c(x, *args): pass
# D
def d(**kwargs): passRecall Solution 1.3
- A — legal.
*argsbefore**kwargsis the correct order. - B — SyntaxError.
**kwargsmust be the last parameter; nothing can follow the catch-all dict. - C — legal. A named positional followed by
*args. - D — legal.
**kwargsalone is fine. Why B fails: once you have a box that absorbs all remaining keywords, there is nothing left for anything after it to catch.
Level 2 — Application
Now you route several arguments to the correct box and compute a result.
Exercise 2.1
Predict the full return value.
def f(a, *args, **kwargs):
return a, args, kwargs
print(f(1, 2, 3, x=9, y=10))Recall Solution 2.1
Fill slots left to right:
atakes the first positional →1.*argssweeps the remaining positionals →(2, 3).**kwargssweeps the keywords with no named slot →{'x': 9, 'y': 10}.- Output:
(1, (2, 3), {'x': 9, 'y': 10})
Exercise 2.2
Write a function product that multiplies any number of arguments, returning 1 for no arguments. Then give product(2, 3, 4) and product().
Recall Solution 2.2
def product(*nums):
total = 1
for x in nums:
total *= x
return totalproduct(2, 3, 4)→2*3*4=24.product()→numsis the empty tuple(), the loop never runs, so we return the startingtotal=1. Why start at 1?1is the multiplicative identity — multiplying by it changes nothing, so it is the correct "empty product," just like0is the correct empty sum.
Exercise 2.3
Use call-site unpacking to make add work from a list and from a dict.
def add(a, b, c):
return a + b + c
nums = [5, 6, 7]
opts = {'a': 10, 'b': 20, 'c': 30}
# fill in with * and **Recall Solution 2.3
add(*nums) # spreads to add(5, 6, 7) -> 18
add(**opts) # spreads to add(a=10, b=20, c=30) -> 60add(*nums)→18.add(**opts)→60. What the star does here (opposite direction): in a call it spreads one collection back into separate arguments, mirroring how it gathered them in a definition.
Level 3 — Analysis
Here you must reason about why a call succeeds, fails, or overrides.
Exercise 3.1
Explain why the first call works and the second raises an error.
def f(a, b):
return a + b
f(*[1, 2]) # call 1
f(*[1, 2, 3]) # call 2Recall Solution 3.1
- Call 1:
*[1, 2]spreads into exactly two positionals →f(1, 2)→3. ✅ - Call 2:
*[1, 2, 3]spreads into three positionals, butfonly has slotsaandb. There is no*argsto catch the extra3, so Python raisesTypeError: f() takes 2 positional arguments but 3 were given. Analysis: unpacking does not forgive the count — it just hands over that many arguments. The receiving header must be able to hold them.
Exercise 3.2
What is final and why?
defaults = {'color': 'red', 'size': 'M', 'qty': 1}
user = {'size': 'L', 'qty': 3}
final = {**defaults, **user}Recall Solution 3.2
final={'color': 'red', 'size': 'L', 'qty': 3}. Why:{**a, **b}poursain first, thenb. When a key appears in both, the later value wins because it is written into the same slot afterwards. Souser'ssize='L'andqty=3overridedefaults, whilecolor(only indefaults) survives untouched.
Exercise 3.3
Trace carefully. What does this print?
def f(a, b=100, *args):
return a, b, args
print(f(1))
print(f(1, 2, 3, 4))Recall Solution 3.3
f(1):a=1,buses its default100,args=(). Output:(1, 100, ()).f(1, 2, 3, 4):a=1,b=2(default overridden), then leftovers3, 4sweep intoargs=(3, 4). Output:(1, 2, (3, 4)). Analysis: a default parameter fills first from the next positional; only after every named slot is satisfied does*argsstart collecting.
Level 4 — Synthesis
Now you build reusable tools that forward arguments they don't understand.
Exercise 4.1
Write call_twice(func, *args, **kwargs) that calls func twice with the same arguments and returns both results in a tuple. Test with pow.
Recall Solution 4.1
def call_twice(func, *args, **kwargs):
return func(*args, **kwargs), func(*args, **kwargs)
call_twice(pow, 2, 3) # (8, 8)- Output:
(8, 8). Why this works for any function: we catch whatever comes afterfuncwith*args, **kwargs, then spread it back withfunc(*args, **kwargs).call_twicenever needs to knowpow's real signature.
Exercise 4.2
Write a debug wrapper (a mini-decorator) that prints the arguments a function was called with, then returns its result unchanged. Test on a small mul.
Recall Solution 4.2
def debug(func):
def wrapper(*args, **kwargs):
print("call", func.__name__, args, kwargs)
result = func(*args, **kwargs)
print("->", result)
return result
return wrapper
@debug
def mul(a, b):
return a * b
mul(6, 7) # prints, then returns 42- Return value of
mul(6, 7)→42. - Printed:
call mul (6, 7) {}then-> 42. Why*args, **kwargsis essential: the wrapper must accept any signature the decorated function might have, so it catches everything generically and forwards it faithfully.
Exercise 4.3
Write merge_settings(base, **overrides) that returns a new dict: base with overrides applied on top (without mutating base). Test it.
Recall Solution 4.3
def merge_settings(base, **overrides):
return {**base, **overrides}
cfg = {'debug': False, 'level': 1}
merge_settings(cfg, level=5, verbose=True)
# {'debug': False, 'level': 5, 'verbose': True}- Output:
{'debug': False, 'level': 5, 'verbose': True}. cfgis unchanged (still{'debug': False, 'level': 1}) because{**base, ...}builds a new dict. Why:**overridescatches caller keywords into a dict, and the merge literal writes overrides last, so they win — exactly the "user beats defaults" pattern.
Level 5 — Mastery
Full reasoning under mixed, tricky rules.
Exercise 5.1
Predict the output. Watch the keyword-only parameter sep.
def join_all(*parts, sep="-"):
return sep.join(parts)
print(join_all("a", "b", "c"))
print(join_all("a", "b", "c", sep="/"))Recall Solution 5.1
sep sits after *args, so it becomes keyword-only — you can only set it by name.
join_all("a", "b", "c"):parts=("a","b","c"),sepkeeps default"-"→"a-b-c".join_all("a", "b", "c", sep="/"): same parts,sep="/"→"a/b/c". Why keyword-only: since*partsgreedily eats every positional, the only way to reachsepis by naming it — a deliberate, useful design.
Exercise 5.2
This raises an error. Say exactly which and why, then fix it so x becomes keyword-only.
def f(*args, x):
return args, x
f(1, 2, 3)Recall Solution 5.2
f(1, 2, 3)→TypeError: f() missing 1 required keyword-only argument: 'x'. Why:xafter*argsis keyword-only and has no default, so it is required by name. Three positionals fillargs=(1,2,3)but nobody suppliesx=....- Fix (make it optional):
def f(*args, x=0):
return args, x
f(1, 2, 3) # ((1, 2, 3), 0)
f(1, 2, x=9) # ((1, 2), 9)f(1, 2, 3)→((1, 2, 3), 0);f(1, 2, x=9)→((1, 2), 9).
Exercise 5.3
Full trace — combine everything. What does the program print?
def report(title, *rows, sep=" | ", **meta):
line = sep.join(str(r) for r in rows)
return title, line, meta
print(report("Scores", 10, 20, 30, sep=", ", author="Ravi", year=2024))Recall Solution 5.3
Route each argument:
title="Scores"(first positional).*rowssweeps remaining positionals →(10, 20, 30).sepnamed explicitly →", "(keyword-only).**metacatches leftover keywords →{'author': 'Ravi', 'year': 2024}.line="10, 20, 30".- Output:
('Scores', '10, 20, 30', {'author': 'Ravi', 'year': 2024}). Why this is the full picture: it exercises the entire legal order — positional,*args, keyword-only,**kwargs— all in one call. See the figure below tracing the routing.

Exercise 5.4
{**a, **b} conflict + unpacking count. Predict both lines.
a = {'p': 1, 'q': 2}
b = {'q': 9, 'r': 3}
print({**a, **b})
def g(p, q, r):
return p + q + r
print(g(**{**a, **b}))Recall Solution 5.4
- Merge:
afirst, thenb; conflictingq→bwins. Result{'p': 1, 'q': 9, 'r': 3}. g(**{...})spreads that dict asg(p=1, q=9, r=3)→1 + 9 + 3=13.- Outputs:
{'p': 1, 'q': 9, 'r': 3}then13.
Connections
- Functions and Parameters — the base every exercise builds on.
- Default Arguments — used in L3 and L5.
- Tuples — what
*argsyields. - Dictionaries — what
**kwargsyields; merge rules in L3/L5. - Decorators — the L4 wrapper pattern.
- Unpacking Assignment — the same star idea in assignments.