Args and kwargs in Python solve two related problems: accepting an unknown number of arguments, and spreading a collection of values out across a call. The same two symbols do both jobs, and the side that you are on decides which meaning applies.
What do *args and **kwargs collect?
When a function declares *args, every extra positional value that no other parameter claims is gathered into a tuple. When it declares **kwargs, every extra keyword value that no other parameter claims is gathered into a dictionary. The names args and kwargs are conventions, not keywords: you could call them *values and **options and the behaviour would be identical.
def show(*args, **kwargs):
print("args:", args, "type:", type(args).__name__)
print("kwargs:", kwargs, "type:", type(kwargs).__name__)
show(1, 2, "three", a=1, b="two")
Two facts follow from that output and will save you time later. The tuple preserves the order in which the values arrived, and the dictionary preserves the order the keywords were written in, because modern Python dictionaries keep insertion order. Neither container is a copy of the caller's objects, though: kwargs is a new dict, but the values inside it are the same objects the caller passed.
How do *args and **kwargs differ from normal parameters?
They sit at the end of the parameter list and they only ever receive what is left over. Normal parameters are matched first, by the binding rules from the previous lesson, and only the surplus flows into the varargs parameter.
def analyse(name, scores, *extra_scores, include_average=True, **metadata):
print("name:", name)
print("scores:", scores)
print("extra_scores:", extra_scores)
print("include_average:", include_average)
print("metadata:", metadata)
analyse("Ada", [90, 80], 70, 60, include_average=False, cohort="2026-01")
Three rules govern where they can appear. *args must come after every ordinary positional parameter. **kwargs must be the very last parameter in the definition. And you cannot have two *args parameters or two **kwargs parameters, because the names would collide.
One subtlety catches people out: a bare * in a signature means "no more positional parameters from here on", and every parameter after it is keyword-only. It does not collect anything.
def configure(path, *, mode="r", encoding="utf-8"):
return f"opening {path} mode={mode} encoding={encoding}"
print(configure("data.csv"))
print(configure("data.csv", mode="rb"))
A call that tries to pass mode positionally fails with TypeError: configure() takes 1 positional argument but 2 were given. That is the intended behaviour, and it is exactly what you want from an option like mode, where swapping it with encoding by accident would be silent and terrible.
Another subtlety deserves a warning label: collecting arguments does not copy the objects inside them. The tuple and the dictionary are new containers, but the objects they point at are the caller's. If a function mutates a list it received through *args, the caller sees the change.
def tag_all(tags, *labels):
tags.extend(labels)
return tags
original = ["draft"]
print(tag_all(original, "final", "reviewed"))
print(original)
What do * and ** do at the call site?
At the call site the two symbols mean the opposite of what they mean in a definition. A single star unpacks an iterable into positional values, and a double star unpacks a mapping into keyword values. This is the mirror image, and it is what lets you hand a function a list or a dictionary you received earlier.
| Position | Symbol | Meaning | Example |
|---|---|---|---|
| Definition | *args | Collect surplus positional values into a tuple | def f(*args) |
| Definition | **kwargs | Collect surplus keyword values into a dict | def f(**kwargs) |
| Call | *iterable | Spread values out as positional arguments | f(*items) |
| Call | **mapping | Spread entries out as keyword arguments | f(**options) |
def send(to, subject="", body="", cc=None):
return f"to={to} subject={subject} body={body} cc={cc}"
print(send(*["[email protected]"], subject="Hi"))
print(send("[email protected]", **{"subject": "Hi", "body": "Welcome"}))
print(send(*["[email protected]", "[email protected]"]))
The third call is worth a second look: unpacking a list of two elements into a function whose first parameter is to and whose second is subject means the second element lands on subject. Unpacking does not know about your intent, only about positions.
Unpacking is not limited to function calls. A list can be built by splicing several lists together, and a dictionary can be built by merging several dictionaries, with later entries winning on conflict.
base = {"theme": "light", "font": "serif"}
extra = {"font": "mono", "lang": "en"}
settings = {**base, **extra}
print(settings)
tags = [*["python", "basics"], *["functions"]]
print(tags)
How do you forward arguments to another function?
The most common reason to write *args, **kwargs is a wrapper that intercepts a call and passes it along. The forwarding idiom is one line, and it preserves the original call's positional and keyword structure exactly.
import time
def timed(func, *args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.6f}s")
return result
def add(a, b):
return a + b
print(timed(add, 2, 3))
print(timed(add, a=2, b=3))
Note that *args and **kwargs in the wrapper's own signature catch the call, and the identical syntax at the inner call site releases them again. Same symbols, opposite direction, one line each.
When you only want to pre-bind a couple of arguments and keep the rest flexible, functools.partial is cleaner than writing a wrapper by hand.
from functools import partial
def greet(greeting, name, punctuation="!"):
return f"{greeting}, {name}{punctuation}"
hello = partial(greet, "Hello")
print(hello("Ada"))
print(hello(name="Grace", punctuation="?"))
When should you avoid *args and **kwargs?
They are convenient, and convenience has a price. A signature of def handler(*args, **kwargs) accepts anything, which means it also accepts things you never intended, such as a misspelled keyword. The typo does not raise an error; it lands in kwargs and is silently ignored, and you spend an hour wondering why the setting had no effect.
Use varargs when the number of values genuinely varies, such as a logging helper or a decorator. Prefer explicit parameters when the shape is known, because they give you autocomplete, type checking and meaningful errors. If a function needs more than a couple of keyword options, a small dataclass or a plain dict parameter passed by name is usually clearer than a free-form **kwargs.
One more edge case to know: ** at a call site requires a mapping, and CPython insists that its keys be strings. Unpacking a list of pairs raises TypeError: argument after ** must be a mapping, not list. Curiously, a string key that is not a valid identifier, such as "a-b", is accepted when the callee declares **kwargs — CPython stores it as-is rather than rejecting it.
Recap
- In a definition,
*argscollects surplus positional values into a tuple and**kwargscollects surplus keyword values into a dict. Both sit at the end of the parameter list. - At a call site,
*spreads an iterable into positional values and**spreads a mapping into keyword values. - Forwarding a call is
func(*args, **kwargs)— the same syntax that captured them, run in the opposite direction. - A bare
*in a definition makes every parameter after it keyword-only, which is the cheapest way to stop callers swapping arguments by accident.
Try it yourself: write def merge_summaries(*summaries, separator=" | ", **metadata) so that it joins the summaries with the separator and stores the metadata in a returned dictionary. Call it once with three positional summaries, once with a list unpacked using *, once with a dictionary unpacked using **, and once with both at the same time. Add one call that passes a keyword the function does not understand and note where it ends up, then decide whether that behaviour is what you want for a real API.