1.2.29 · D4Introduction to Programming (Python)

Exercises — ` - args` and ` - kwargs` — flexible argument passing

2,319 words11 min readBack to topic

Quick refresher of the only rule you truly need:

Figure — ` - args` and ` - kwargs` — flexible argument passing

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): pass
Recall Solution 1.3
  • A — legal. *args before **kwargs is the correct order.
  • B — SyntaxError. **kwargs must be the last parameter; nothing can follow the catch-all dict.
  • C — legal. A named positional followed by *args.
  • D — legal. **kwargs alone 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:

  • a takes the first positional → 1.
  • *args sweeps the remaining positionals → (2, 3).
  • **kwargs sweeps 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 total
  • product(2, 3, 4)2*3*4 = 24.
  • product()nums is the empty tuple (), the loop never runs, so we return the starting total = 1. Why start at 1? 1 is the multiplicative identity — multiplying by it changes nothing, so it is the correct "empty product," just like 0 is 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) -> 60
  • add(*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 2
Recall 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, but f only has slots a and b. There is no *args to catch the extra 3, so Python raises TypeError: 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} pours a in first, then b. When a key appears in both, the later value wins because it is written into the same slot afterwards. So user's size='L' and qty=3 override defaults, while color (only in defaults) 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, b uses its default 100, args=(). Output: (1, 100, ()).
  • f(1, 2, 3, 4): a=1, b=2 (default overridden), then leftovers 3, 4 sweep into args=(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 *args start 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 after func with *args, **kwargs, then spread it back with func(*args, **kwargs). call_twice never needs to know pow'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, **kwargs is 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}.
  • cfg is unchanged (still {'debug': False, 'level': 1}) because {**base, ...} builds a new dict. Why: **overrides catches 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"), sep keeps default "-""a-b-c".
  • join_all("a", "b", "c", sep="/"): same parts, sep="/""a/b/c". Why keyword-only: since *parts greedily eats every positional, the only way to reach sep is 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: x after *args is keyword-only and has no default, so it is required by name. Three positionals fill args=(1,2,3) but nobody supplies x=....
  • 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).
  • *rows sweeps remaining positionals → (10, 20, 30).
  • sep named explicitly → ", " (keyword-only).
  • **meta catches 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.
Figure — ` - args` and ` - kwargs` — flexible argument passing

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: a first, then b; conflicting qb wins. Result {'p': 1, 'q': 9, 'r': 3}.
  • g(**{...}) spreads that dict as g(p=1, q=9, r=3)1 + 9 + 3 = 13.
  • Outputs: {'p': 1, 'q': 9, 'r': 3} then 13.


Connections

  • Functions and Parameters — the base every exercise builds on.
  • Default Arguments — used in L3 and L5.
  • Tuples — what *args yields.
  • Dictionaries — what **kwargs yields; merge rules in L3/L5.
  • Decorators — the L4 wrapper pattern.
  • Unpacking Assignment — the same star idea in assignments.