Tech With Galvan

*args and **kwargs Demystified

20 min • Beginner • Module 7

Accept any arguments — the pattern behind every flexible API.

*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

  1. *args — “collect all extra positional arguments into a tuple”
  2. **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: *args before normal params — normal first, then *args, then **kwargs.
  • kwargs keys are strings — kwargs["name"], never kwargs[name] (unless you have a variable holding “name”).

✅ Checkpoint

  1. What do *args and **kwargs collect into? (Tuple and dict)
  2. Correct parameter order? *(Normal → *args → *kwargs)
  3. What does introduce(*("Diya", 15)) do? (Unpacks the tuple into two positional arguments)
  4. 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.