*args and **kwargs in Python

โฑ๏ธ 40 sec read ๐Ÿ Python

*args collects any extra positional arguments into a tuple, and **kwargs collects any extra keyword arguments into a dict. They let a function accept a variable number of arguments โ€” the names args and kwargs are just convention; the * and ** do the work.

What Does *args Do?

Inside the function, args is a plain tuple holding every positional argument beyond the named parameters, so the function works with 0, 1, or 50 values.

def total(*args):
    return sum(args)

total(1, 2)          # 3
total(1, 2, 3, 4)    # 10
total()              # 0 โ€” args is an empty tuple

What Does **kwargs Do?

kwargs is a dict mapping each extra keyword argument's name to its value, preserving the order they were passed in.

def build_url(base, **kwargs):
    query = "&".join(f"{k}={v}" for k, v in kwargs.items())
    return f"{base}?{query}" if query else base

build_url("/search", q="pandas", page=2)
# '/search?q=pandas&page=2'

What Is the Correct Parameter Order?

Parameters must appear in this order: regular positional, *args, keyword-only parameters, then **kwargs. Anything defined after *args can only be passed by keyword.

def f(a, b=2, *args, mode="fast", **kwargs):
    ...

f(1, 2, 3, 4, mode="slow", debug=True)
# a=1, b=2, args=(3, 4), mode="slow", kwargs={"debug": True}

# def g(**kwargs, *args):  <- SyntaxError, ** must come last

How Does Unpacking with * and ** Work?

The same symbols work in reverse at the call site: * spreads a sequence into positional arguments and ** spreads a dict into keyword arguments.

coords = (3, 7)
opts = {"mode": "slow", "debug": True}

f(*coords, **opts)          # f(3, 7, mode="slow", debug=True)

# Also handy for merging dicts:
defaults = {"timeout": 30, "retries": 3}
config = {**defaults, "retries": 5}    # {'timeout': 30, 'retries': 5}

How Do You Forward Arguments to a Wrapped Function?

The classic use is a wrapper that passes everything through untouched: accept *args, **kwargs, forward them with *args, **kwargs. This is the backbone of every decorator โ€” see Python decorators for the full pattern.

import functools, time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)      # forward everything
        print(f"{func.__name__}: {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

Common Pitfalls

Pro Tip: A bare * in the signature forces keyword-only arguments without accepting extras: def resize(image, *, width, height) makes resize(img, 800, 600) an error, so nobody swaps width and height by accident.

โ† Back to Python Tips