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
Last updated: 24 September 2026
*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
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))tuple dict
laptop
tuple dict
laptop | sale, new | brand=Dell, ram_gb=16name 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:
| Part | Example | Passed as |
|---|---|---|
| Positional-only | a before / | Position only |
| Standard | b | Position or keyword |
| Extra positionals | *args | Collected into a tuple |
| Keyword-only | c after * or *args | Keyword only |
| Extra keywords | **kwargs | Collected 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:
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)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:
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}))tuple dict
laptop | refurbished / warranty | brand=Lenovo
would send 750 EURsep 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:
try:
describe("laptop", sep=",", **options)
except TypeError as exc:
print(exc)__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:
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"))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:
def export(path, **options):
overwrite = options.get("overwrite", False)
return f"export to {path}, overwrite={overwrite}"
print(export("report.csv", overwirte=True))export to report.csv, overwrite=FalseThe 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?
*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?
How do I pass a list to a function that expects separate arguments?
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?
Summary
- In a definition,
*argscollects extra positional arguments into a tuple and**kwargscollects extra keyword arguments into a dictionary. - In a call,
*and**unpack a sequence and a dictionary into arguments. - Parameters after
*or*argsare keyword-only; parameters before/are positional-only. - Use
*args, **kwargsto 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
- More on Defining Functions (tutorial) — Python documentation
- PEP 3102 – Keyword-Only Arguments — Python Enhancement Proposals
- PEP 570 – Python Positional-Only Parameters — Python Enhancement Proposals
- PEP 692 – Using TypedDict for more precise **kwargs typing — Python Enhancement Proposals
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.
Related articles
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.
Python for Data Science: The Essential Libraries
The Python data science stack explained by the job each library does: NumPy for arrays, pandas for tables, Matplotlib and seaborn for charts, SciPy and statsmodels for statistics, scikit-learn for models and Jupyter for exploration, with one runnable example that passes a real dataset through them.