*args and **kwargs Demystified
Sometimes a function should accept any number of inputs — print() does it, max() does it, and after this lesson, yours can too. The syntax looks cryptic; the idea is two sentences.
The two sentences
*args— “collect all extra positional arguments into a tuple”**kwargs— “collect all extra keyword arguments into a dict”
That’s genuinely the whole concept. Watch:
def demonstrate(*args, **kwargs):
print("args:", args)
print("kwargs:", kwargs)
demonstrate(1, 2, 3, name="Aarav", age=16)
Output:
args: (1, 2, 3)
kwargs: {'name': 'Aarav', 'age': 16}
The star gathers leftovers into a tuple; the double star gathers leftovers into a dict. Inside the function, they’re ordinary containers — loop them, index them, len() them.
A real example: add them all up
def add_all(*numbers):
total = 0
for n in numbers:
total += n
return total
print(add_all(1, 2)) # 3
print(add_all(1, 2, 3, 4, 5)) # 15
print(add_all()) # 0 — empty tuple, no problem
Compare with fixed parameters — add(a, b) handles exactly two, forever. add_all(*numbers) handles zero, two, or two hundred. Flexibility is the point.
args and normal parameters can mix
Normal parameters first, then *args collects the overflow:
def introduce(greeting, *names):
for name in names:
print(f"{greeting}, {name}!")
introduce("Hello", "Aarav", "Diya", "Kabir")
Output:
Hello, Aarav!
Hello, Diya!
Hello, Kabir!
"Hello" went to the named parameter; everything after landed in names. The order rule: **normal params → *args → kwargs, always in that order.
Where you’ve already used this (twice!)
Every function you’ve defined with a flexible body:
def wrapper(*args, **kwargs): # from the decorators world
...
And the built-ins you’ve called all course:
print("a", "b", "c", sep="-") # print accepts *values + **kwargs
max(4, 9, 2) # max accepts any count
Now you know how they work — the stars gather, the function loops.
Unpacking: stars work in reverse too
The stars also spread values out at call time:
def introduce(name, age):
print(f"{name} is {age}")
info = ("Diya", 15)
introduce(*info) # unpacks the tuple into two arguments!
settings = {"name": "Kabir", "age": 16}
introduce(**settings) # unpacks the dict into keyword arguments
*info turns a tuple into positional arguments; **settings turns a dict into keyword arguments. This pairing — gather with stars in the definition, spread with stars in the call — is how Streamlit and every flexible library passes settings around.
Naming: the convention
args and kwargs are just names — the stars are the magic:
def add_all(*values): # totally fine
def config(**options): # also fine
But *args, **kwargs is the universal convention — stick to it unless a better name truly helps.
Common Errors & Fixes
TypeError: introduce() takes 2 positional arguments but 3 were given— the function lacks*args; add it if extra inputs are legal.- Order crash:
*argsbefore normal params — normal first, then *args, then **kwargs. - kwargs keys are strings —
kwargs["name"], neverkwargs[name](unless you have a variable holding “name”).
✅ Checkpoint
- What do
*argsand**kwargscollect into? (Tuple and dict) - Correct parameter order? *(Normal → *args → *kwargs)
- What does
introduce(*("Diya", 15))do? (Unpacks the tuple into two positional arguments) - Name a built-in you’ve used that takes *args. (print, max)
Next: scope — where variables live, why they vanish, and the LEGB rule in five minutes.