Python *args and **kwargs Explained: Flexible Functions With Clear Examples
Those stars in function signatures are less mysterious than they look. Learn how *args and **kwargs collect arguments, how the same stars unpack them, and how to design function signatures that are hard to misuse.
*argscollects extra positional arguments into a tuple.**kwargscollects extra keyword arguments into a dictionary.- The names are only a convention, as the
*and**are what matter. - The same stars unpack a list or dictionary when you call a function.
Sooner or later, you will read a function like def handler(*args, **kwargs) and wonder what it accepts. Python *args and **kwargs let a function take a flexible number of arguments. They power decorators, wrappers and many library APIs.
This guide covers how they work in both directions, along with the newer ways Python lets you control how a function is called. We ran each example on Python 3.13, and the output shown comes from that run.
What *args does
A parameter written with one star collects any extra positional arguments into a tuple:
def total(*args):
print(type(args).__name__, args)
return sum(args)
print(total(1, 2, 3))
# tuple (1, 2, 3)
# 6
print(total())
# tuple ()
# 0
With no arguments, you simply get an empty tuple, so the function still works. The name args is a convention. *numbers would behave the same, and a more descriptive name is often better.
What **kwargs does
Two stars collect any extra keyword arguments into a dictionary:
def describe(**kwargs):
print(type(kwargs).__name__, kwargs)
describe(name="Asha", city="Pune")
# dict {'name': 'Asha', 'city': 'Pune'}
Because it is a normal dictionary, you can loop over it, look up keys with get() and pass it on. Keys keep the order in which they were passed.
Parameter order
When you mix kinds of parameters, Python requires a specific order:
- Regular positional parameters
*args- Keyword-only parameters
**kwargs
def order(item, qty=1, *extras, priority=False, **notes):
print(item, qty, extras, priority, notes)
order("pen")
# pen 1 () False {}
order("pen", 2, "gift wrap", "card", priority=True, note="leave at door")
# pen 2 ('gift wrap', 'card') True {'note': 'leave at door'}
Notice priority. Because it comes after *extras, it can only be set by name, so a stray positional argument could never set it by accident. Putting **kwargs earlier is not allowed, and our test raised a SyntaxError: “arguments cannot follow var-keyword argument”.
Unpacking when calling
The same stars work in reverse at the call site. One star spreads a list or tuple into positional arguments, and two stars spread a dictionary into keyword arguments:
nums = [4, 5, 6]
total(*nums) # same as total(4, 5, 6) -> 15
settings = {"name": "Ben", "city": "Delhi"}
describe(**settings) # same as describe(name="Ben", city="Delhi")
Unpacking works with any function, not only those that use *args and **kwargs. That is handy when configuration arrives as a dictionary:
def connect(host, port, timeout=5):
return f"{host}:{port} (timeout {timeout})"
connect(**{"host": "db.local", "port": 5432})
# 'db.local:5432 (timeout 5)'
connect(**{"host": "db.local", "port": 5432, "ssl": True})
# TypeError: connect() got an unexpected keyword argument 'ssl'
That error is useful. A function without **kwargs rejects unknown options, so a typo in a setting name fails loudly instead of being silently ignored.
Keyword-only arguments
A bare * in the signature, with no name, means “everything after this must be passed by keyword”:
def make_user(name, *, admin=False):
return name, admin
make_user("Asha", admin=True)
# ('Asha', True)
make_user("Asha", True)
# TypeError: make_user() takes 1 positional argument but 2 were given
This is a great way to protect boolean flags. A call like make_user("Asha", True) forces readers to guess what True means. Requiring admin=True makes every call self-explanatory.
Positional-only arguments
Since Python 3.8, a / marks parameters that can only be passed by position:
def area(width, height, /):
return width * height
area(3, 4) # 12
area(width=3, height=4)
# TypeError: area() got some positional-only arguments
# passed as keyword arguments: 'width, height'
This lets a library rename those parameters later without breaking anyone’s code, since nobody can depend on the names. Several built-ins work this way. For example, len(obj=[1]) does not work.
Tip: The full pattern is def f(pos_only, /, normal, *, kw_only). You rarely need all three, but knowing what / and * mean helps you read documentation and library signatures.
Decorators and wrappers
This is the main real-world reason to use both together. A wrapper often needs to accept whatever arguments the original function takes, and pass them on unchanged:
import functools, time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
return result
return wrapper
@timed
def slow_add(a, b, delay=0.01):
time.sleep(delay)
return a + b
print(slow_add(2, 3, delay=0.02))
# slow_add took 0.0205s
# 5
The wrapper does not need to know slow_add‘s parameters at all, so it works for any function. Your timing figure will differ slightly. Our guide to Python decorators explains functools.wraps and other details.
Merging options
** also works inside a dictionary literal, which gives a neat way to apply overrides on top of defaults:
defaults = {"theme": "light", "lang": "en"}
def build(**overrides):
return {**defaults, **overrides}
build(theme="dark")
# {'theme': 'dark', 'lang': 'en'}
Later keys win, so the override replaces the default. Subclasses also use this pattern, as in super().__init__(*args, **kwargs), to pass extra arguments through to a parent class. See Python classes and objects for more on super().
Cheat sheet
| Syntax | Where | Meaning |
|---|---|---|
def f(*args) |
Definition | Collect extra positional arguments into a tuple |
def f(**kwargs) |
Definition | Collect extra keyword arguments into a dict |
def f(a, *, b) |
Definition | b is keyword-only |
def f(a, /, b) |
Definition | a is positional-only |
f(*items) |
Call | Spread a sequence into positional arguments |
f(**options) |
Call | Spread a dict into keyword arguments |
{**a, **b} |
Literal | Merge dictionaries |
[*a, *b] |
Literal | Combine iterables into a list |
When to avoid them
Flexibility has a price. A function defined as def create(**kwargs) tells readers nothing about which options exist, and editors cannot autocomplete them. Typos may pass silently through several layers before anything fails.
So prefer explicit parameters when you know what a function needs. Use *args and **kwargs when you genuinely need flexibility: wrappers, decorators, forwarding arguments to another function, or a true variable-length input like total(*numbers). If a function needs many named options, a dataclass can also be clearer.
Common mistakes
Using a mutable default
This is not about stars, but it often appears near them. A default value is created once, when the function is defined, and then shared between calls:
def bad(items=[]):
items.append(1)
return items
print(bad(), bad())
# [1, 1] [1, 1]
Both calls returned the very same list, which by then held two items. Use items=None and create a new list inside the function.
Other errors
- Forgetting to unpack. Calling
total(nums)passes one list rather than three numbers. Writetotal(*nums). - Swallowing typos. A function that accepts
**kwargsbut ignores unknown keys hides misspelled options. Validate them, or avoid**kwargs. - Passing the same argument twice.
f(1, **{"a": 2}), whereais the first parameter, raises a “multiple values” error. - Not forwarding in a wrapper. Calling
func(args, kwargs)passes a tuple and a dict. The correct form isfunc(*args, **kwargs).
Type hints for *args and **kwargs
Type hints on *args and **kwargs describe each individual value, not the whole collection. This surprises many people:
def total(*prices: float) -> float:
return sum(prices) # prices is tuple[float, ...]
def tag(**attrs: str) -> str:
return " ".join(f'{k}="{v}"' for k, v in attrs.items()) # dict[str, str]
total(1.5, 2.5) # 4.0
tag(id="x", role="button") # id="x" role="button"
Here prices is treated as a tuple of floats and attrs as a dictionary of strings. When keyword arguments have different types, Python 3.12 added a precise option: a TypedDict combined with Unpack.
from typing import TypedDict, Unpack
class Options(TypedDict, total=False):
timeout: float
retries: int
def fetch(url: str, **opts: Unpack[Options]) -> None: ...
A type checker such as mypy or Pyright can then reject fetch(url, retries="3") or a misspelt option name, which a plain **kwargs can never do. Runtime behaviour does not change, though. When we ran fetch("u", retries="3") under Python 3.13, it executed without complaint, because Python itself does not enforce type hints.
If you find yourself documenting a long list of accepted keys for **kwargs, consider replacing them with explicit keyword-only parameters. Editors can then autocomplete them, and mistakes are caught immediately.
Final thoughts
Remember the two directions. In a definition, stars collect. In a call, they unpack. Add keyword-only arguments to make calls clearer, and reserve **kwargs for code that genuinely forwards arguments. To practise, write a small decorator that logs every call’s arguments, and use it on a few different functions.
Frequently asked questions
What is the difference between *args and **kwargs?
*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dictionary. A function can use both together.
Do I have to call them args and kwargs?
No. The stars do the work, so *values or **options are equally valid. args and kwargs are simply a widely recognised convention.
What order should *args and **kwargs go in?
Regular parameters first, then *args, then keyword-only parameters, and **kwargs last. Any other order causes a SyntaxError.
What does a single * mean in a function definition?
A bare * means every parameter after it must be passed by keyword. It makes calls clearer, especially for boolean flags.
What does / mean in a Python function?
Parameters before / are positional-only, so callers cannot pass them by name. This feature was added in Python 3.8.
Keep learning
Need help with testing, WordPress or SEO?
We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.