datarekha
Python Easy Asked at GoogleAsked at AmazonAsked at Meta

What is the difference between *args and **kwargs, and when would you use each?

The short answer

*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.

Learn it properly Functions

Keep practising

All Python questions

Explore further

Skip to content