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 —
**kwargsmust come last;*argsbefore keyword defaults. - Confusing definition vs call — in a
def,*collects; in a call,*spreads. - Passing a positional after
**kwargsat call time — keyword args must follow positional ones. - Forgetting
argsis a tuple andkwargsis a dict — treat them accordingly. - Overusing them — explicit named parameters are clearer when the signature is known; reserve
*args/**kwargsfor genuinely variable input or forwarding.
Summary Table
| Syntax | In a definition | At a call |
|---|---|---|
*args | Collect extra positionals into a tuple | Spread an iterable into positionals |
**kwargs | Collect extra keywords into a dict | Spread a dict into keywords |
| Order | regular, *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.
π¬ Comments (0)
No comments yet. Be the first to share your thoughts!