Question bank — ` - args` and ` - kwargs` — flexible argument passing
Before we start, three anchors so no symbol is unearned:
The picture behind every trap
Before you attack the traps, burn this routing diagram into memory. When Python receives a call, it sorts arguments left-to-right into three bins: named slots first, then the *args tuple sweeps up leftover positionals, then the **kwargs dict sweeps up leftover keywords.

Look at the arrows: the burnt-orange values flow into the tuple, the teal name=value pairs flow into the dict, and only the plum-labelled named slots are filled first. Almost every trap below is just this diagram with one arrow going where you didn't expect.
True or false — justify
Star collects positionals into a list.
*args produces a tuple, which is immutable — you cannot args.append(x); you must do list(args) first if you want to mutate.**kwargs produces a tuple of pairs.
kwargs["x"] works and kwargs.items() gives (name, value) pairs.The names args and kwargs are keywords Python requires.
* and ** carry meaning; def f(*stuff, **options) is perfectly valid — args/kwargs are just convention for readability.def f(a, *args, b) makes b a normal positional parameter.
*args becomes keyword-only: b must be passed as b=..., because *args already swallowed every remaining positional value.You may write def f(**kwargs, *args).
SyntaxError. A catch-all dict must be the last parameter; nothing can follow something that absorbs all remaining keywords.Calling add(nums) with nums=[1,2,3] is the same as add(*nums).
add(nums) passes the list as one argument; add(*nums) spreads it into three separate positional arguments. Forgetting the * is a classic bug.In {**a, **b}, if both dicts share a key, a wins.
b overrides a on shared keys — dict literals evaluate left to right, and each key overwrites the previous.*args and **kwargs can only appear in a function definition.
A function def f(*args) will crash if called with zero arguments.
args simply becomes the empty tuple (); no error. Always guard division/indexing against this empty case.The name-collision trap (worked out)
The one subtle case people trip on: can a keyword argument "collide" with a value already caught by *args? The answer is a firm no — they live in separate bins, and the code below shows exactly why. Trace it against the routing diagram above.
Spot the error
def f(a, **kwargs, b=2): ...
**kwargs is not last — SyntaxError. Move it to the end: def f(a, b=2, **kwargs).def total(*args): return args + 10
args is a tuple, and tuple + int is a TypeError. You meant sum(args) + 10 — you must reduce the tuple to a number first.config = {"lr": 0.1}; train(*config)
train("lr")), almost never what you want. Use train(**config) to spread key=value pairs.def g(*args, **kwargs, extra): ...
**kwargs is illegal — SyntaxError. Keyword-only params must sit between *args and **kwargs, e.g. def g(*args, extra, **kwargs).log(func, *args, **kwargs); func(args, kwargs)
func receives a tuple and a dict as two positional values instead of the spread-out arguments. Correct forwarding is func(*args, **kwargs).def h(a, *args): return a; h(*[])
h(*[]) spreads an empty list, so nothing fills the required a → TypeError: missing 1 required positional argument. * on an empty sequence supplies zero arguments.def m(**kwargs): return kwargs[0]
kwargs is a dict keyed by strings, not an integer-indexed sequence; kwargs[0] raises KeyError. Use kwargs["name"] or iterate kwargs.items().Why questions
Why must **kwargs always come last in a definition?
Why does args come back as a tuple and not a list?
Why does the same * symbol both pack and unpack?
f(*x) where def f(*x) visually symmetric.Why do parameters after *args become keyword-only?
*args starts eating every remaining positional value, no positional slot is left for later parameters — so the only way to reach them is by name.Why is *args, **kwargs the standard way decorators forward calls?
*args, **kwargs and re-spreading with func(*args, **kwargs) forwards any signature untouched.Why does def f(a, b, *args) still require a and b?
*args only collects the leftover positionals after the named ones are satisfied. The required a, b are filled first; only then does the surplus flow into args.Why prefer {**a, **b} over calling a.update(b) to merge dicts?
a; update changes a in place. Same override rule (later wins), different side effect.Edge cases
The traps below all live at the boundaries — empty inputs, bare stars, mixed iterables. The figure walks four of them side by side so you can see the memory-model outcome rather than simulate it in your head.

What is args when you call f() where def f(*args)?
(). Not None, not an error — an empty container you can safely loop over (zero times).What does f(*[42]) bind if def f(x)?
x=42. Unpacking works for any length, including one and zero.Can def f(*args, **kwargs) accept a call with no arguments at all?
args becomes () and kwargs becomes {} — the most permissive signature possible, accepting anything including nothing.What happens with f(1, 2, a=3) when def f(a, *args)?
TypeError: a is filled positionally by 1, then a=3 tries to fill it again — a single parameter can't be bound both by position and by keyword.If def f(a, *, b) (bare star), what does the lone * do?
b must be given as b=..., and f(1, 2) is an error.What does def f(a, *args, b, **kwargs) demand at minimum?
a (positional) and b (keyword-only, because it sits after *args) are both required; args and kwargs may be empty. So f(1, b=2) is the minimal valid call.Does unpacking f(*gen) work if gen is a generator, not a list?
*, including generators, strings, and sets (order for sets is arbitrary). The star consumes the iterable position by position.Connections
- Functions and Parameters — the parameter rules these traps stress-test.
- Default Arguments — interact with keyword-only slots after
*args. - Tuples — what
*argsbecomes (immutable → several traps). - Dictionaries — what
**kwargsbecomes. - Decorators — the forwarding traps in "Spot the error".
- Unpacking Assignment — same star idea outside function calls.