What is the difference between *args and **kwargs, and when would you use each?
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. Use *args when the number of positional inputs is unknown, **kwargs when callers should be able to pass named options without modifying the function signature.
How to think about it
The names args and kwargs are pure convention — all the meaning lives in the prefix. A single * says “gather the leftover positional arguments into a tuple”; a double ** says “gather the leftover keyword arguments into a dict.” Once you read * and ** as “scoop up the extras,” the feature stops being two special names and becomes one small idea.
It exists because a Python signature is normally fixed, yet plenty of functions can’t know their inputs in advance — a logger that accepts any message, a decorator that wraps any function, a trainer that forwards whatever config it is handed. *args and **kwargs are how a function stays open to that.
A worked example
Inside the function, args is always an ordinary tuple and kwargs an ordinary dict — nothing magical once they land. The one rule to respect is the order they may appear in a signature: regular parameters, then *args, then keyword-only parameters, then **kwargs.
def report(*args, **kwargs):
print("Positional args:", args)
print("Keyword kwargs: ", kwargs)
print()
report(1, 2, 3, model="xgboost", metric="auc")
# Full ordering: a normal param, *args, a keyword-only param, then **kwargs
def build_model(name, *layers, verbose=False, **hyperparams):
print(f"Model: {name}")
print(f"Layers: {layers}")
print(f"Verbose: {verbose}")
print(f"Hyperparams: {hyperparams}")
print()
build_model("mlp", 128, 64, 32, verbose=True, lr=0.001, dropout=0.2)
# The same * and ** unpack a collection back into arguments at the call site
base_params = {"lr": 0.01, "epochs": 10}
report("run_1", **base_params)
# A bare * collects nothing — it just forces 'verbose' to be keyword-only
def fit(X, y, *, verbose=False):
print(f"fit called, verbose={verbose}")
fit([1, 2], [0, 1], verbose=True)
Positional args: (1, 2, 3)
Keyword kwargs: {'model': 'xgboost', 'metric': 'auc'}
Model: mlp
Layers: (128, 64, 32)
Verbose: True
Hyperparams: {'lr': 0.001, 'dropout': 0.2}
Positional args: ('run_1',)
Keyword kwargs: {'lr': 0.01, 'epochs': 10}
fit called, verbose=True
Notice the symmetry. In a definition, * and ** gather extras into a tuple and a dict; at a call site, the same * and ** spread a tuple or dict back out into arguments. One pair of symbols, working in both directions.
Where it really earns its keep
The payoff is forwarding. When a decorator’s inner wrapper is written wrapper(*args, **kwargs), it hands along everything the caller gave — including arguments the original author never anticipated. That is exactly what keeps the wrapper transparent, and exactly why functools.wraps exists to carry the original name and signature across with it.