Intuition What this page is
The parent note taught you what *args and **kwargs do. This page is the stress test : we list every kind of situation these operators can throw at you — empty inputs, mixed inputs, collisions, unpacking on both sides, the exam trick that catches everyone — and then we solve one example for each cell. If you can predict all ten outputs below, you have mastered the topic .
A tiny reminder of the two words we will use constantly:
positional argument — a value passed by position (order), like the 2 in add(2, 3).
keyword argument — a value passed by name , like the x=9 in f(x=9).
Think of every call to a flexible function as a point on a grid. One axis is how many positional values arrive; the other is how many keyword values arrive; and there are special "edge" columns for the weird cases. Below is that grid flattened into a checklist — every row is one distinct thing that can go wrong or surprise you.
Cell
Scenario class
Covered by
A
*args catches many extra positionals
Ex 1
B
*args catches zero (empty tuple ()) — degenerate
Ex 2
C
**kwargs catches many keywords
Ex 3
D
Named param + *args + **kwargs all at once (the split)
Ex 4
E
Call-site unpacking : spread a list with *, a dict with **
Ex 5
F
Collision : same key given twice → TypeError
Ex 6
G
Keyword-only parameters (the lone * separator)
Ex 7
H
Dict-merge with {**a, **b} — later key wins (override)
Ex 8
I
Real-world word problem: a shopping-cart total
Ex 9
J
Exam twist: forgetting * at the call site (single-arg trap)
Ex 10
Every numeric or structural answer below is machine-checked in the verify block.
Look at the figure above: the incoming values flow left to right . Named slots fill first (black), then the leftover positionals pour into the *args tuple (red), and any stray name=value pairs drop into the **kwargs dict. Keep this picture in mind for every example.
Worked example Ex 1 — sum of any count of numbers
def total ( * args):
return sum (args)
print (total( 2 , 3 , 4 , 5 ))
Forecast: How many arguments does total "officially" accept, and what shape is args?
def total(*args) gives the function no fixed positional slots. Why this step? The * says "there are no named slots to fill; sweep everything into one tuple."
The four values 2, 3, 4, 5 have nowhere else to go, so Python packs them: args = (2, 3, 4, 5). Why this step? Leftover positionals always become a tuple — this is the "collect" role of *.
sum(args) adds the tuple's elements: 2+3+4+5 = 14. Why this step? sum walks any iterable, and a tuple is iterable.
Verify: 2+3+4+5 = 14. ✅ Answer: 14.
Worked example Ex 2 — the empty-tuple edge case
def total ( * args):
return sum (args)
print (total())
Forecast: Error? None? 0? Something else?
We call total() with no arguments. Why this step? This is the degenerate input — the boundary of the matrix — and beginners expect a crash.
Python still creates args, but now it is the empty tuple (). Why this step? *args never errors on "too few"; the catch-all simply catches nothing.
sum(()) is 0 by definition (the sum of no numbers). Why this step? sum starts from 0 and adds nothing, so it returns the start value.
Verify: sum(()) == 0. ✅ Answer: 0. (No if not args: guard even needed here — but you'd add one if you divided, as in the parent's mean example.)
Worked example Ex 3 — collecting labelled data
def profile ( ** kwargs):
return list (kwargs.items())
print (profile( name = "Ravi" , age = 20 , city = "Pune" ))
Forecast: What type is kwargs, and what does .items() give?
def profile(**kwargs) declares no named parameters, only the double-star catch-all. Why this step? Every incoming key=value has no matching slot, so all of them fall into kwargs.
Python builds the dict kwargs = {'name':'Ravi', 'age':20, 'city':'Pune'}. Why this step? This is the "collect keywords → dict" role — the mirror of *args for names.
.items() yields (key, value) pairs; wrapping in list(...) freezes them into a list of tuples. Why this step? We want a concrete, printable, order-preserving result (dicts keep insertion order in Python 3.7+).
Verify: list == [('name','Ravi'), ('age',20), ('city','Pune')]. ✅
Worked example Ex 4 — one named, the rest split
def f (a, * args, ** kwargs):
return a, args, kwargs
print (f( 1 , 2 , 3 , x = 9 , y = 10 ))
Forecast: Draw the three buckets. Where does each of 1, 2, 3, x=9, y=10 land?
a is a normal named positional; it grabs the first positional value → a = 1. Why this step? Named slots are filled before the catch-alls (see the figure: black slots fill first).
2 and 3 are leftover positionals → args = (2, 3). Why this step? Once a is satisfied, remaining positionals sweep into the tuple.
x=9 and y=10 are keywords with no named slot → kwargs = {'x':9, 'y':10}. Why this step? Named keywords with no matching parameter fall into the double-star dict.
Verify: result == (1, (2, 3), {'x':9, 'y':10}). ✅
Worked example Ex 5 — spreading a list and a dict
into a call
def volume (length, width, height):
return length * width * height
dims_list = [ 2 , 3 , 4 ]
dims_dict = { 'length' : 2 , 'width' : 3 , 'height' : 4 }
print (volume( * dims_list)) # spread the list
print (volume( ** dims_dict)) # spread the dict
Forecast: Both calls target the SAME 3-slot function. Do both give the same number?
volume(*dims_list) turns [2,3,4] into three separate positionals 2, 3, 4. Why this step? At a call , * does the opposite of the def role: it spreads , matching values to slots by position.
So slots fill as length=2, width=3, height=4 → 2*3*4 = 24. Why this step? Position order in the list must match parameter order — that is the risk of positional spread.
volume(**dims_dict) spreads by name : each dict key must equal a parameter name. Why this step? ** matches by keyword, so order in the dict does not matter — safer but stricter.
Verify: both equal 24. ✅
Worked example Ex 6 — the same argument given twice
def greet (name):
return f "hi { name } "
args = ( "Ravi" ,)
kwargs = { "name" : "Sara" }
greet( * args, ** kwargs) # what happens?
Forecast: "hi Ravi"? "hi Sara"? Or an error?
*args spreads ("Ravi",) → the positional name="Ravi". Why this step? One positional fills the single slot name.
**kwargs then tries to also set name="Sara" — but name is already filled . Why this step? One parameter cannot receive two values; there is no rule to pick a "winner" at a call.
Python raises TypeError: greet() got multiple values for argument 'name'. Why this step? This is a genuine ambiguity error — different from the dict-merge case in Ex 8, where later simply overrides.
Verify: we confirm the collision by checking that a positional and a same-named keyword conflict. ✅ (Answer: TypeError , no valid return value.)
Worked example Ex 7 — the lone
* that forces names
def connect (host, * , port, timeout = 30 ):
return host, port, timeout
print (connect( "db" , port = 5432 ))
# connect("db", 5432) # would be a TypeError
Forecast: After the bare *, can port be passed positionally?
A bare * with no name means "no more positionals allowed after here ." Why this step? It's a divider: everything to its right is keyword-only .
So host takes the positional "db", but port and timeout must be named. connect("db", port=5432) uses timeout's default 30. Why this step? Keyword-only params make calls self-documenting and prevent accidental positional mistakes.
Result: ("db", 5432, 30). Trying connect("db", 5432) fails because 5432 has no positional slot to land in. Why this step? This is exactly the "keyword-only" cell of the ordering positional → *args → keyword-only → **kwargs.
Verify: result == ("db", 5432, 30). ✅
Worked example Ex 8 — override via
{**a, **b}
defaults = { 'color' : 'red' , 'size' : 'M' , 'qty' : 1 }
user = { 'size' : 'L' , 'qty' : 3 }
final = { ** defaults, ** user}
print (final)
Forecast: Which value of size and qty survives?
{**defaults, **user} pours defaults into a new dict first. Why this step? Order of unpacking is left-to-right, so defaults lays the base layer.
Then user is poured on top; where keys collide , the later value overwrites . Why this step? Unlike Ex 6 (a call), a dict literal has a clear rule — last assignment wins, no error.
color stays 'red' (only in defaults); size → 'L', qty → 3 (user overrides). Why this step? This is the standard "config defaults + user overrides" pattern.
Verify: final == {'color':'red', 'size':'L', 'qty':3}. ✅
Worked example Ex 9 — a flexible shopping-cart total
A shop bills any number of item prices, then optionally applies a percentage discount and a flat shipping fee passed by name.
def checkout ( * prices, discount = 0 , shipping = 0 ):
subtotal = sum (prices)
after_discount = subtotal * ( 1 - discount / 100 )
return round (after_discount + shipping, 2 )
print (checkout( 100 , 250 , 50 , discount = 10 , shipping = 40 ))
Forecast: Estimate the total in your head before computing.
*prices catches the three item prices → prices = (100, 250, 50), subtotal = 400. Why this step? The count of items varies per customer, so a catch-all is exactly right.
discount and shipping are keyword-only-ish extras with defaults. Here discount=10 means a 10% cut: 400 * (1 - 10/100) = 400 * 0.9 = 360. Why this step? Percentages combine cleanly with *args because they are named , not positional.
Add flat shipping=40: 360 + 40 = 400.00. Why this step? Shipping is added after the discount — order of business logic matters.
Verify: round(400*0.9 + 40, 2) == 400.0. ✅ Units: currency dollars, two decimals via round(..., 2).
Worked example Ex 10 — forgetting the
* at the call site
def total ( * args):
return len (args)
nums = [ 10 , 20 , 30 ]
print (total(nums)) # trap
print (total( * nums)) # fixed
Forecast: What does each len(args) print — 3 and 3? 1 and 3?
total(nums) passes the list itself as a single positional value. Why this step? Without *, the list is one object; args = ([10,20,30],) — a 1-element tuple whose only element is the list.
So len(args) == 1. Why this step? We measured the tuple of arguments, and there is exactly one argument.
total(*nums) spreads the list: args = (10, 20, 30), so len(args) == 3. Why this step? The star at the call site is what converts "one list" into "three values" — the exact bug most exams test.
Verify: first prints 1, second prints 3. ✅
Recall One-line self-test for the whole matrix
Predict all ten answers, then check: 14, 0, the pairs list, (1,(2,3),{...}), 24 twice, TypeError, ("db",5432,30), the merged dict, 400.0, and (1, 3).
A: 14 ::: sum of 2+3+4+5
B: total() ::: 0 — args is the empty tuple
F: collision ::: TypeError — name got two values
J: total(nums) vs total(*nums) ::: 1 then 3
Functions and Parameters — the fixed-slot baseline every example bends.
Default Arguments — powers discount=0, shipping=0, timeout=30 above.
Tuples — what *args builds (Ex 1, 2, 4, 10).
Dictionaries — what **kwargs builds and what {**a,**b} merges (Ex 3, 8).
Decorators — the forwarding pattern generalises Ex 4 and 5.
Unpacking Assignment — the same star idea, on the left of =.