Python *args and **kwargs

Sometimes you don’t know in advance how many arguments a function will need to accept, or what they’ll be called. Python solves this with two special parameter forms: *args, which gathers any extra positional arguments into a tuple, and **kwargs, which gathers any extra keyword arguments into a dictionary. They’re used everywhere in real Python code — from simple utility functions to decorators, wrappers, and framework APIs like Django views or `print()` itself — so understanding them well is essential to reading and writing idiomatic Python.

Overview / How it works

The names args and kwargs are not keywords — they’re just a strong convention. What actually matters to Python is the * and ** operators placed before a parameter name in a function definition. These operators tell Python: “collect whatever is left over here.” You could legally write *values or **options instead, and many libraries do use more descriptive names, but sticking to *args/**kwargs makes your code instantly recognizable to other Python developers.

Internally, when a function is called, Python matches the arguments in this order:

  • Named positional and keyword-or-positional parameters are filled first, in order.
  • Any remaining positional arguments (ones with no named parameter left to bind to) are packed into a tuple and assigned to the *args parameter.
  • Any keyword-only parameters (declared after *args) are filled by name.
  • Any remaining keyword arguments that don’t match a named parameter are packed into a regular dict and assigned to the **kwargs parameter, in the order they were passed (Python dicts preserve insertion order since 3.7).

Because args ends up as an ordinary tuple and kwargs as an ordinary dict inside the function body, you can loop over them, index into args, call len(), use .items() on kwargs, or unpack them further — they behave exactly like any tuple or dict you’d create yourself. A function definition may have at most one *args and one **kwargs parameter, and *args must always come before **kwargs in the parameter list.

The same * and ** operators also work in reverse, at the call site, where they mean “unpack this collection into separate arguments” rather than “pack extra arguments into a collection.” This dual role — packing in a definition, unpacking in a call — is what makes *args/**kwargs so useful for writing generic, pass-through code.

Syntax

def function_name(pos1, pos2, *args, kw_only=None, **kwargs):
    ...
Part Meaning
pos1, pos2 Regular positional (or keyword) parameters, matched first.
*args Collects extra positional arguments into a tuple named args.
kw_only=None A keyword-only parameter — anything after *args can only be passed by name.
**kwargs Collects extra keyword arguments into a dict named kwargs.

On the calling side, the same symbols unpack existing collections: func(*a_list) spreads a list or tuple out as positional arguments, and func(**a_dict) spreads a dict out as keyword arguments (the dict’s keys must be strings that match parameter names).

Examples

Example 1: Summing an unknown number of values

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

print(total(1, 2, 3))
print(total(10, 20, 30, 40))
print(total())

Output:

6
100
0

Each call passes a different number of positional arguments, and Python collects them all into the args tuple before the function body even runs. Calling total() with no arguments simply produces an empty tuple, and sum(()) is 0 — there’s no need for a special case.

Example 2: Building output from arbitrary keyword arguments

def print_profile(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_profile(name="Ava", age=29, city="Boston")

Output:

name: Ava
age: 29
city: Boston

None of name, age, or city is declared in the function signature, so all three land in the kwargs dictionary, in the order they were supplied at the call site.

Example 3: Combining regular, *args, keyword-only, and **kwargs parameters

def build_report(title, *sections, author="Unknown", **metadata):
    print(f"Report: {title}")
    print(f"Author: {author}")
    for i, section in enumerate(sections, start=1):
        print(f"Section {i}: {section}")
    for key, value in metadata.items():
        print(f"{key}: {value}")

build_report(
    "Sales Q2",
    "Overview",
    "Regional Breakdown",
    author="Priya",
    version=2,
    confidential=True,
)

Output:

Report: Sales Q2
Author: Priya
Section 1: Overview
Section 2: Regional Breakdown
version: 2
confidential: True

title takes the first positional argument, "Overview" and "Regional Breakdown" are extra positionals that land in the sections tuple, author is a keyword-only parameter with a default, and version/confidential don’t match any named parameter so they end up in metadata. This is the pattern real-world APIs use to accept a required core argument plus an open-ended set of options.

Under the Hood: Unpacking at the Call Site

The mirror image of packing is unpacking, which happens when you call a function. If you already have a list or dict of values, you don’t have to write them out one by one — you can spread them with * or **:

def describe(a, b, c):
    print(f"{a}, {b}, {c}")

nums = [1, 2, 3]
describe(*nums)

data = {"a": 10, "b": 20, "c": 30}
describe(**data)

Output:

1, 2, 3
10, 20, 30

Step by step, describe(*nums) tells Python to take each element of nums and pass it as its own positional argument — equivalent to writing describe(1, 2, 3). Likewise, describe(**data) takes each key/value pair and passes it as key=value, matching the dict’s keys against describe‘s parameter names. This is exactly how generic wrapper functions and decorators forward arguments: they collect calls with *args, **kwargs in their own signature, then re-unpack the same args and kwargs to call the wrapped function, without ever needing to know its actual parameter list.

Common Mistakes

Mistake 1: Assuming *args also captures keyword arguments

*args only ever collects positional arguments. Passing keyword arguments to a function that has no **kwargs raises a TypeError:

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

try:
    result = add_numbers(a=1, b=2, c=3)
    print(result)
except TypeError as e:
    print(f"Error: {e}")

Output:

Error: add_numbers() got an unexpected keyword argument 'a'

The fix is to also accept **kwargs if you genuinely want to support both call styles:

def add_numbers(*args, **kwargs):
    return sum(args) + sum(kwargs.values())

result = add_numbers(1, 2, a=1, b=2, c=3)
print(result)

Output:

9

Mistake 2: Unpacking a dict whose keys aren’t strings

** unpacking requires every key to be a valid keyword name, which means a string. Non-string keys raise a TypeError at the call site, before the function even runs:

def show(**kwargs):
    print(kwargs)

data = {1: "a", 2: "b"}
try:
    show(**data)
except TypeError as e:
    print(f"Error: {e}")

Output:

Error: keywords must be strings

The fix is to make sure the dict you’re unpacking actually has string keys:

def show(**kwargs):
    print(kwargs)

data = {"1": "a", "2": "b"}
show(**data)

Output:

{'1': 'a', '2': 'b'}

Mistake 3: Wrong parameter order

*args must always appear before **kwargs in a function definition, and no parameter (other than keyword-only ones) can follow **kwargs. Writing it the other way around is a SyntaxError that Python refuses to even parse:

def process(a, **kwargs, *args):
    pass

Python reports this as invalid syntax because **kwargs must be the very last parameter in the signature; reorder it to def process(a, *args, **kwargs): to fix it.

Best Practices

  • Prefer explicit, named parameters whenever the set of arguments is known in advance — *args/**kwargs trade discoverability for flexibility, so use them only when that flexibility is genuinely needed.
  • When a function’s only job is to accept arbitrary arguments and forward them to another function (a decorator, a proxy, a logging wrapper), *args, **kwargs is exactly the right tool — forward them with the same *args, **kwargs syntax at the inner call.
  • Document what keys you expect inside **kwargs in the docstring, since the function signature alone won’t show them to callers or IDEs.
  • Place required, explicit parameters before *args, and use keyword-only parameters (declared after *args) for options that should never be passed positionally by accident.
  • Be aware that a typo in a keyword argument silently ends up inside **kwargs instead of raising an error, which can hide bugs; validate known keys explicitly if that risk matters for your API.
  • Avoid combining a mutable default argument with **kwargs merging in a way that mutates the caller’s dictionary — build a new dict with {**defaults, **kwargs} instead of mutating defaults in place.
  • Use *args and **kwargs together in decorators so the decorator works with any wrapped function, regardless of its own signature.

Practice Exercises

  • Write a function multiply_all(*args) that returns the product of all the numbers passed to it. Decide, and test, what it should return when called with no arguments at all.
  • Write a function build_url(base, **params) that takes a base URL string and any number of keyword arguments, and returns a query string like "https://example.com?key1=value1&key2=value2" built from params.
  • Write a decorator function log_call(func) that returns a wrapper accepting *args, **kwargs, prints the function’s name together with the arguments it was called with, then calls func(*args, **kwargs) and returns its result.

Summary

  • *args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict.
  • args and kwargs are just conventional names — the * and ** operators are what matter.
  • Parameter order in a definition is: regular positional/keyword params, then *args, then keyword-only params, then **kwargs.
  • The same */** operators unpack an existing list/tuple or dict into separate arguments at a call site.
  • *args never captures keyword arguments, and **kwargs unpacking requires string keys — both raise TypeError otherwise.
  • Use them for flexible, pass-through functions like decorators and wrappers, but prefer explicit parameters when the arguments are known in advance.