Python *args and **kwargs Explained Clearly

What *args and **kwargs really are: a tuple and a dictionary that collect extra arguments, and the same symbols unpacking values when you call a function. With the full parameter order, positional-only and keyword-only markers, forwarding arguments through wrappers, and the silent typo that **kwargs can swallow.

GloryTecks Team

Training & Placement Faculty

6 May 2026 6 min read

Last updated: 24 September 2026

Share:

*args and **kwargs look like special syntax, but they are two simple ideas. In a function definition, * collects extra positional arguments into a tuple and ** collects extra keyword arguments into a dictionary. In a function call, the same symbols do the reverse: they unpack a sequence or a dictionary into separate arguments. The names args and kwargs are only conventions. Once that clicks, the rest is detail, and this guide covers the details that matter in real code.

Collecting: the definition side

python
def describe(name, *tags, sep=", ", **attrs):
    print(type(tags).__name__, type(attrs).__name__)
    parts = [name]
    if tags:
        parts.append(sep.join(tags))
    if attrs:
        parts.append(sep.join(f"{k}={v}" for k, v in attrs.items()))
    return " | ".join(parts)

print(describe("laptop"))
print(describe("laptop", "sale", "new", brand="Dell", ram_gb=16))
text
tuple dict
laptop
tuple dict
laptop | sale, new | brand=Dell, ram_gb=16

name takes the first positional argument; everything positional after it lands in the tuple tags; keyword arguments that match no named parameter land in the dictionary attrs, in the order they were passed. When nothing extra is passed, you get an empty tuple and an empty dictionary, never None. The Python tutorial calls these arbitrary argument lists (Python tutorial: defining functions).

Note sep: because it comes after *tags, it can only be passed by keyword. Otherwise describe("laptop", "sale", " / ") would be ambiguous: is " / " a tag or the separator?

The full order of parameters

A signature can contain up to five kinds of parameter, always in this order:

PartExamplePassed as
Positional-onlya before /Position only
StandardbPosition or keyword
Extra positionals*argsCollected into a tuple
Keyword-onlyc after * or *argsKeyword only
Extra keywords**kwargsCollected into a dictionary

A bare * with no name marks the start of keyword-only parameters without collecting anything (PEP 3102), and / marks the end of positional-only ones (PEP 570). Both make call sites clearer and protect you from mistakes:

python
def transfer(amount, /, *, currency="INR", dry_run=False):
    return f"{'would send' if dry_run else 'sent'} {amount} {currency}"

print(transfer(500, currency="USD", dry_run=True))
for bad_call in (lambda: transfer(500, "USD"), lambda: transfer(amount=500)):
    try:
        bad_call()
    except TypeError as exc:
        print(exc)
text
would send 500 USD
transfer() takes 1 positional argument but 2 were given
transfer() got some positional-only arguments passed as keyword arguments: 'amount'

transfer(500, "USD") fails because currency is keyword-only. A reader of transfer(500, True) could never tell what True meant; with the *, the call must say dry_run=True. The / is mostly for library authors: it frees them to rename amount later without breaking anyone who might otherwise have called transfer(amount=500).

Unpacking: the call side

At a call, * spreads a sequence into positional arguments and ** spreads a dictionary into keyword arguments:

python
tags = ["refurbished", "warranty"]
options = {"sep": " / ", "brand": "Lenovo"}
print(describe("laptop", *tags, **options))

defaults = {"currency": "INR", "dry_run": True}
overrides = {"currency": "EUR"}
print(transfer(750, **{**defaults, **overrides}))
text
tuple dict
laptop | refurbished / warranty | brand=Lenovo
would send 750 EUR

sep matched the named parameter, and brand went into **attrs. The last line shows a common pattern: merging dictionaries with {**a, **b}, where later keys win, then unpacking the result into a call. Passing the same keyword twice, once directly and once through **, is an error rather than a silent override:

python
try:
    describe("laptop", sep=",", **options)
except TypeError as exc:
    print(exc)
text
__main__.describe() got multiple values for keyword argument 'sep'

The real use: forwarding arguments

The most important use of *args, **kwargs is passing arguments through code that does not need to know what they are. Decorators are the classic example: the wrapper must accept whatever the wrapped function accepts and hand it on unchanged:

python
import functools

def logged(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__} args={args} kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@logged
def area(width, height, unit="m"):
    return f"{width * height} {unit}²"

print(area(3, 4, unit="cm"))
text
calling area args=(3, 4) kwargs={'unit': 'cm'}
12 cm²

The wrapper works for any function, whatever its signature. Subclasses that extend a parent method, and functions that pass options on to a library call, use the same pattern. Python decorators with real examples builds several decorators on it.

The danger: **kwargs swallows typos

A function that accepts **kwargs and only looks for certain keys will accept anything, including mistakes:

python
def export(path, **options):
    overwrite = options.get("overwrite", False)
    return f"export to {path}, overwrite={overwrite}"

print(export("report.csv", overwirte=True))
text
export to report.csv, overwrite=False

The caller asked to overwrite, misspelled the option, and got the opposite with no error. If a function has a known set of options, declare them as keyword-only parameters, def export(path, *, overwrite=False), and the typo becomes a TypeError immediately. Keep **kwargs for code that genuinely forwards arguments it does not interpret, and for APIs designed to accept open-ended attributes.

Type hints for *args and **kwargs

The annotation applies to each item, not to the container: *args: int means every extra positional argument is an int, and **kwargs: str means every extra keyword value is a string. When keyword arguments have different types, Python 3.12 added a precise form, **kwargs: Unpack[SomeTypedDict], which gives each allowed keyword its own type (PEP 692). For more on annotations, see Python type hints.

Reading real signatures

Once you know the five kinds of parameter, the signatures in Python's documentation read like sentences. Take print(*objects, sep=' ', end='\n', file=None, flush=False). The *objects is why print("a", "b", 3) accepts any number of values, and why sep and end must be passed by keyword: they come after the star. It is also why print(*items) prints a list's elements separated by spaces, while print(items) prints the list itself: the star unpacks the list into separate arguments. Likewise, dict(**kwargs) explains why dict(city="Pune", pin=411001) builds a dictionary: the keywords are collected and become its keys.

Do the names have to be args and kwargs?

No. Only the star and double star matter. Descriptive names often read better, such as *paths for a function that takes any number of file paths, or **options for settings passed through to another library.

Can a function have two *args parameters?

No. A signature can have at most one starred parameter and one double-starred parameter, and the double-starred one must come last.

How do I pass a list to a function that expects separate arguments?

Unpack it with a star at the call site: max(*readings) passes each reading as its own argument. Many built-ins also accept the list directly, as max(readings) does, so check the signature first.

Are *args and **kwargs slow?

The cost of packing a tuple or dictionary is tiny compared with almost any real work a function does. Choose them for clarity and flexibility, not for speed.

Summary

  • In a definition, *args collects extra positional arguments into a tuple and **kwargs collects extra keyword arguments into a dictionary.
  • In a call, * and ** unpack a sequence and a dictionary into arguments.
  • Parameters after * or *args are keyword-only; parameters before / are positional-only.
  • Use *args, **kwargs to forward arguments you do not interpret; use explicit keyword-only parameters for options you do.

These techniques sit alongside the built-in collections they produce; see the Python data structures reference for tuples and dictionaries in depth. Function arguments and parameters, return values, lambdas and recursion form the functions module of the Python Programming course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Python?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course