
*args and **kwargs in Python: Flexible Function Signatures
You've seen def wrapper(*args, **kwargs): in decorators, and probably super().__init__(**kwargs) in class hierarchies. These two parameters let a function accept any number of positional arguments and any number of keyword arguments, which is how Python's own print(), max(), and dict() can take as many values as you pass them.
The names args and kwargs are only a convention. What matters is the * and **. A single star collects extra positional arguments into a tuple, and a double star collects extra keyword arguments into a dict. The same symbols used at a call site do the reverse and spread a sequence or dict into separate arguments.
This post covers how *args and **kwargs collect arguments, the full order of parameter kinds (including keyword-only and positional-only parameters), unpacking at the call site, forwarding arguments in decorators and subclasses, how to type-hint all of it, and when a flexible signature is the wrong choice.
*args: Collecting Positional Arguments
Put a * before a parameter name and it gathers every positional argument that isn't claimed by an earlier parameter:
def total(*numbers: float) -> float:
print(type(numbers).__name__, numbers)
return sum(numbers)
print(total(1, 2, 3))
print(total())
tuple (1, 2, 3)
6
tuple ()
0
Two things to notice:
- The collected values arrive as a tuple, not a list. It's immutable, so if you need to modify it, convert it with
list(numbers). - Calling with no extra arguments is fine. The tuple is simply empty.
The type hint *numbers: float describes each argument, not the tuple. Inside the function, numbers has the type tuple[float, ...].
You can combine *args with regular parameters. Required ones come first and get filled before anything is collected:
def first(*args):
return args[0] if args else None
print(first(), first(9))
None 9
**kwargs: Collecting Keyword Arguments
A ** parameter gathers every keyword argument that doesn't match a named parameter, as a dict:
def tag(name: str, **attrs: str) -> str:
print(type(attrs).__name__, attrs)
parts = "".join(f' {k}="{v}"' for k, v in attrs.items())
return f"<{name}{parts}>"
print(tag("a", href="/blog", title="Blog"))
dict {'href': '/blog', 'title': 'Blog'}
<a href="/blog" title="Blog">
name is matched normally; href and title don't match any parameter, so they land in attrs. Since Python 3.7, dicts preserve insertion order, so the keyword arguments appear in the order the caller wrote them.
As with *args, **attrs: str annotates each value. The keys are always strings.
The dict is a fresh object built for each call, so modifying it inside the function doesn't affect a dict the caller spread into the call:
def mutable_kwargs(**kw):
kw["x"] = 1
return kw
src = {"y": 2}
print(mutable_kwargs(**src), src)
{'y': 2, 'x': 1} {'y': 2}
Keyword-Only Parameters
Anything declared after *args can only be passed by keyword, because *args has already swallowed every remaining positional argument:
def log(level: str, *messages: str, sep: str = " ") -> str:
return f"[{level}] " + sep.join(messages)
print(log("INFO", "server", "started"))
print(log("INFO", "a", "b", sep=" | "))
print(log("INFO", "a", "b", " | "))
[INFO] server started
[INFO] a | b
[INFO] a b |
The third call shows why this design is necessary. Passing " | " positionally doesn't set sep; it becomes another message. This is exactly how print() works: sep and end are keyword-only so they're never mistaken for things to print.
You can get keyword-only parameters without accepting extra positional arguments by using a bare *:
def connect(host: str, *, port: int = 5432, timeout: float = 5.0) -> str:
return f"{host}:{port} (timeout={timeout})"
print(connect("db.local", port=6543))
connect("db.local", 6543)
db.local:6543 (timeout=5.0)
TypeError: connect() takes 1 positional argument but 2 were given
The bare * marks "everything after this is keyword-only". It's one of the most useful tools for API design: connect("db.local", 6543, 10) would be unreadable, and forcing keywords keeps call sites self-documenting. It also lets you reorder or add options later without breaking callers.
Positional-Only Parameters
The mirror image, added in Python 3.8 (PEP 570), is /. Parameters before a / can only be passed positionally:
def ratio(a: float, b: float, /) -> float:
return a / b
print(ratio(1, 4))
ratio(a=1, b=4)
0.25
TypeError: ratio() got some positional-only arguments passed as keyword arguments: 'a, b'
Why restrict this? Two reasons:
- Parameter names become an implementation detail. If callers can't use
a=, you can renamealater without breaking anyone. - It avoids collisions with
**kwargs. This one is subtle and worth seeing:
def f(name, **kwargs):
return name, kwargs
f("x", **{"name": "dup"})
TypeError: f() got multiple values for argument 'name'
name was filled positionally and then again by keyword. If you mark name as positional-only, a name keyword has nowhere else to go, so it lands in kwargs:
def g(name, /, **kwargs):
return name, kwargs
print(g("x", name="dup"))
('x', {'name': 'dup'})
That's important for functions that accept arbitrary keyword data, like a template renderer or a function that builds a record from field names chosen by the caller.
The Full Parameter Order
Every kind of parameter has a fixed position in the signature:
def full(a, b=2, /, c=3, *args, d, e=5, **kwargs):
return a, b, c, args, d, e, kwargs
print(full(1, d=4))
print(full(1, 20, 30, 40, 50, d=4, z=9))
(1, 2, 3, (), 4, 5, {})
(1, 20, 30, (40, 50), 4, 5, {'z': 9})
Reading left to right:
| Part | Kind | Passed as |
|---|---|---|
a, b=2 before / | Positional-only | Position only |
c=3 | Positional-or-keyword | Either |
*args | Variadic positional | Extra positions, as a tuple |
d, e=5 after *args | Keyword-only | Keyword only (d is required) |
**kwargs | Variadic keyword | Extra keywords, as a dict |
Note that d is required even though it comes after c=3, which has a default. That's allowed for keyword-only parameters, which aren't bound by the "no required parameter after a default" rule that positional ones follow. **kwargs must always be last.
You can inspect any function's signature with inspect.signature():
import inspect
print(inspect.signature(full))
(a, b=2, /, c=3, *args, d, e=5, **kwargs)
Unpacking at the Call Site
At a call site, * and ** do the opposite job: they spread an iterable into positional arguments and a mapping into keyword arguments.
def call(*args, **kwargs):
return args, kwargs
point = (3, 4)
opts = {"color": "red"}
print(call(*point, **opts))
print(call(*point, 5, *[6], flag=True, **opts))
((3, 4), {'color': 'red'})
((3, 4, 5, 6), {'flag': True, 'color': 'red'})
You can mix spreads with regular arguments and use several of each in one call. A couple of rules apply:
- Keys spread with
**must be strings:call(**{1: "x"})raisesTypeError: keywords must be strings. - The same keyword can't arrive twice. Spreading two dicts that share a key is an error, not a merge:
call(**{"a": 1}, **{"a": 2})
TypeError: __main__.call() got multiple values for keyword argument 'a'
If you want later values to override earlier ones, merge first: call(**(defaults | overrides)). The broader topic of unpacking in assignments and literals is covered in Unpacking in Python.
Forwarding Arguments
The most common real-world use of *args, **kwargs is a function that doesn't care about the arguments itself and just passes them along.
In Decorators
A decorator's wrapper has to accept whatever the decorated function accepts. *args, **kwargs does that without knowing the signature:
import functools
import time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return wrapper
@timed
def add(a: int, b: int = 0) -> int:
return a + b
print(add(2, b=3))
add took 0.0000s
5
(The exact time will vary.) The wrapper collects (2,) and {"b": 3}, then spreads them back into the original call, so add sees exactly what the caller passed. functools.wraps copies the name, docstring, and signature metadata so the wrapper doesn't hide the original function. Decorators get the full treatment in Python Decorators Explained.
In Cooperative Inheritance
With multiple inheritance, each class's __init__ takes the arguments it needs and forwards the rest up the method resolution order:
from typing import Any
class Base:
def __init__(self, name: str, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.name = name
class Timestamped:
def __init__(self, *, created: str = "now", **kwargs: Any) -> None:
super().__init__(**kwargs)
self.created = created
class Doc(Base, Timestamped):
pass
d = Doc(name="readme", created="2026-09-15")
print(d.name, d.created)
readme 2026-09-15
Base takes name and passes created on; Timestamped takes created and passes nothing on to object. If a caller passes something nobody consumes, object.__init__() complains:
Doc(name="x", colour="red")
TypeError: object.__init__() takes exactly one argument (the instance to initialize)
That error is a feature, but the message is unhelpful. It's one reason to forward **kwargs only where you actually need cooperative inheritance. Inheritance and super() in Python explains how the MRO decides who gets called next.
Type Hints for Flexible Signatures
Plain annotations cover the simple cases: *args: int means every positional extra is an int, and **kwargs: str means every keyword value is a str. Two newer features handle the harder ones.
Preserving a Signature with ParamSpec
An untyped decorator erases the wrapped function's signature as far as type checkers are concerned. ParamSpec captures the parameters so the wrapper can declare "same arguments as the original":
import functools
from collections.abc import Callable
def logged[**P, R](func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"calling {func.__name__} with {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
@logged
def greet(name: str, punctuation: str = "!") -> str:
return f"Hello, {name}{punctuation}"
print(greet("Ada", punctuation="?"))
calling greet with ('Ada',) {'punctuation': '?'}
Hello, Ada?
The def logged[**P, R] syntax is the type parameter syntax from Python 3.12 (PEP 695). On older versions, declare P = ParamSpec("P") and R = TypeVar("R") from typing instead. Either way, a type checker now knows that greet("Ada", 5) is an error even though the call goes through the wrapper.
Typing Specific Keyword Arguments with Unpack
When **kwargs accepts a known set of keys with different types, describe them with a TypedDict and Unpack (PEP 692, Python 3.12+):
from typing import NotRequired, TypedDict, Unpack
class Options(TypedDict):
retries: int
verbose: NotRequired[bool]
def fetch(url: str, **kwargs: Unpack[Options]) -> str:
return f"{url} {kwargs}"
print(fetch("https://example.com", retries=3))
https://example.com {'retries': 3}
At runtime nothing changes, but type checkers now flag misspelled options and wrong value types. This is a good middle ground when you want a long list of optional settings without spelling every one out as a separate parameter in several functions. More on this in Generics, Protocols, and TypedDict.
When Not to Use *args and **kwargs
Flexible signatures have real costs:
- They hide the API.
def create(**kwargs)tells callers nothing; they have to read the body to find out which keys matter. IDE autocompletion andhelp()show nothing useful either. - Typos pass silently.
create(colour="red")instead ofcoloris accepted and ignored unless you validate. - Errors surface far away. In a forwarding chain, a bad argument fails deep inside another function with a confusing message.
So prefer explicit parameters whenever you know what a function accepts. Reserve *args for genuinely variadic operations (like max() or os.path.join()), and **kwargs for forwarding, cooperative inheritance, and open-ended data such as HTML attributes or query filters.
If you do accept arbitrary keywords, validate them:
def configure(**options):
defaults = {"debug": False, "level": "INFO"}
unknown = options.keys() - defaults.keys()
if unknown:
raise TypeError(f"unexpected options: {sorted(unknown)}")
return defaults | options
print(configure(debug=True))
configure(debgu=True)
{'debug': True, 'level': 'INFO'}
TypeError: unexpected options: ['debgu']
Dict key views support set operations, so options.keys() - defaults.keys() finds unrecognized names in one line.
Also watch out for mutable default values on the explicit parameters that sit alongside *args and **kwargs; that's a separate trap covered in Default Argument Pitfalls in Python.
Conclusion
*args collects extra positional arguments into a tuple and **kwargs collects extra keyword arguments into a dict. Parameters after *args (or a bare *) are keyword-only, parameters before / are positional-only, and together these let you design signatures that are both flexible and hard to misuse. At the call site, * and ** spread sequences and dicts back into arguments, which is what makes transparent forwarding in decorators and super().__init__(**kwargs) chains possible.
Use them where flexibility is the point, add ParamSpec or Unpack[TypedDict] so type checkers can still follow along, and fall back to plain named parameters everywhere else. A signature that lists its arguments is still the best documentation a function can have.


