Ever seen def func(*args, **kwargs): and wondered what the stars mean? They let a function accept any number of arguments — the secret behind flexible APIs, decorators, and wrappers throughout Python and its frameworks. Once you understand them, a huge amount of "magic" library code becomes readable.

This guide builds up from ordinary parameters to *args (extra positional arguments) and **kwargs (extra keyword arguments), the correct ordering rules, and the mirror-image unpacking operators that spread a list or dict into a call. Every example is runnable with output.

A Quick Recap: Positional and Keyword Arguments

Normally you pass arguments by position or by name, and parameters can have defaults.

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Tushar"))                 # positional
print(greet("Asha", greeting="Hi"))    # keyword

Output:

Hello, Tushar!
Hi, Asha!

*args β€” Any Number of Positional Arguments

Prefix a parameter with * and it collects all extra positional arguments into a tuple. The name args is just convention — the * is what matters.

def total(*args):
    print(type(args), args)
    return sum(args)

print(total(1, 2, 3))
print(total(10, 20, 30, 40))
print(total())

Output:

<class 'tuple'> (1, 2, 3)
6
<class 'tuple'> (10, 20, 30, 40)
100
<class 'tuple'> ()
0

**kwargs β€” Any Number of Keyword Arguments

Prefix a parameter with ** and it collects all extra keyword arguments into a dictionary.

def make_profile(**kwargs):
    print(type(kwargs), kwargs)
    for key, value in kwargs.items():
        print(f"{key} = {value}")

make_profile(name="Tushar", age=25, city="Surat")

Output:

<class 'dict'> {'name': 'Tushar', 'age': 25, 'city': 'Surat'}
name = Tushar
age = 25
city = Surat

Combining Everything: The Ordering Rule

When you mix parameter kinds, they must appear in this order: regular → *args → keyword-with-defaults → **kwargs.

def order(a, b, *args, sep="-", **kwargs):
    print("a, b:", a, b)
    print("args:", args)
    print("sep:", sep)
    print("kwargs:", kwargs)

order(1, 2, 3, 4, sep="|", x=10, y=20)

Output:

a, b: 1 2
args: (3, 4)
sep: |
kwargs: {'x': 10, 'y': 20}

The Mirror: Unpacking with * and **

The same operators work in the other direction at the call site: * spreads a list/tuple into positional arguments, and ** spreads a dict into keyword arguments.

def point(x, y, z):
    return f"({x}, {y}, {z})"

coords = [1, 2, 3]
print(point(*coords))            # spread list -> positional

values = {"x": 10, "y": 20, "z": 30}
print(point(**values))           # spread dict -> keyword

Output:

(1, 2, 3)
(10, 20, 30)

Why This Powers Decorators and Wrappers

The classic reason to learn *args/**kwargs: a wrapper that must forward whatever arguments the original function takes, no matter its signature.

from functools import wraps

def debug(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with {args} {kwargs}")
        return func(*args, **kwargs)     # forward everything
    return wrapper

@debug
def add(a, b, c=0):
    return a + b + c

print(add(2, 3, c=5))

Output:

Calling add with (2, 3) {'c': 5}
10

Real-World Example: A Flexible Logger

*args/**kwargs let you write a function that adapts to many call styles — here, a logger that accepts any message parts and any metadata.

def log(*parts, level="INFO", **meta):
    message = " ".join(str(p) for p in parts)
    extra = " ".join(f"{k}={v}" for k, v in meta.items())
    print(f"[{level}] {message}  {extra}".strip())

log("User", "logged in", level="DEBUG", user_id=42, ip="10.0.0.1")
log("Server started")

Output:

[DEBUG] User logged in  user_id=42 ip=10.0.0.1
[INFO] Server started

Common Mistakes to Avoid

  • Wrong order**kwargs must come last; *args before keyword defaults.
  • Confusing definition vs call — in a def, * collects; in a call, * spreads.
  • Passing a positional after **kwargs at call time — keyword args must follow positional ones.
  • Forgetting args is a tuple and kwargs is a dict — treat them accordingly.
  • Overusing them — explicit named parameters are clearer when the signature is known; reserve *args/**kwargs for genuinely variable input or forwarding.

Summary Table

SyntaxIn a definitionAt a call
*argsCollect extra positionals into a tupleSpread an iterable into positionals
**kwargsCollect extra keywords into a dictSpread a dict into keywords
Orderregular, *args, kw_defaults, **kwargs

Conclusion

*args and **kwargs give your functions flexibility: accept any number of positional or keyword arguments, and forward them transparently. The same */** operators unpack collections back into calls. Together they are the foundation of decorators, wrappers, and adaptable library APIs.

Try writing a print_all(*args) function and a wrapper that times any function using *args, **kwargs — you'll feel how much flexibility two little symbols unlock.