In a definition, *args collects unmatched positional arguments into a tuple and **kwargs collects unmatched keyword arguments into a dictionary.
A catch-all signature hides accepted options, and forwarding it blindly can duplicate keywords, preserve misspellings, or pass data across the wrong API boundary.
Keep named parameters explicit, use * and ** only at genuinely variable boundaries, and validate or remove wrapper-owned options before forwarding the rest.
What it is and why it exists
*args and **kwargs are conventional spellings for two kinds of variadic parameter . In a function definition, one leading * collects surplus positional arguments and two leading ** collect surplus keyword arguments. The names are ordinary names: *items and **options behave the same way and are often more descriptive.
A parameter is an input slot declared by a function. An argument is the value or expression supplied by a caller. Keeping those words separate makes the rules easier to state: Python binds arguments to parameters, then packs any arguments left over into the variadic parameters.
This syntax solves two real interface problems. Some operations naturally accept a variable number of homogeneous values, such as a formatter receiving several fields. Wrappers and decorators may also need to forward calls whose exact shape belongs to another callable.
Variadic parameters are not a substitute for designing an interface. If timeout, retries, and headers are supported options, naming them in the signature makes spelling errors fail immediately and gives editors, documentation tools, and type checkers useful information. Reach for **kwargs when the accepted key set really is open or belongs to a downstream callable.
The same star tokens have a complementary meaning at a call site. *iterable supplies its elements as positional arguments, while **mapping supplies its string-keyed entries as keyword arguments. This is argument unpacking , not the packing performed by variadic parameters in a definition.
You meet both directions in decorators, adapters, class cooperation through super(), test parametrization helpers, and APIs that combine fixed controls with an extensible payload. The important design question is where the flexible boundary begins and which layer owns each option.
How it works
Python classifies every parameter in a function signature . The / and * separators, together with variadic parameters, determine how a call may supply each value.
| Parameter kind | Definition shape | Accepted call form |
|---|---|---|
| Positional-only | before / | by position |
| Positional-or-keyword | after /, before * | by position or keyword |
| Variadic positional | *items | zero or more positional arguments |
| Keyword-only | after * or *items | by keyword |
| Variadic keyword | **options | zero or more unmatched keyword arguments |
Consider this signature:
def render(template, /, context=None, *fragments, escape=True, **attributes): ...
template is positional-only. context may be positional or keyword, fragments receives additional positional arguments, escape is keyword-only, and attributes receives other keywords. A bare * can mark keyword-only parameters when the function does not need to collect extra positional arguments.
Positional binding
Python assigns each positional argument to available non-keyword-only parameters from left to right. After those slots are filled, a variadic positional parameter receives the remaining values as a new tuple. With no surplus values, that parameter is an empty tuple.
Parameters before / are positional-only. Their source names are implementation details rather than callable keyword names. This is useful when a public parameter name may change, or when the same spelling should remain available inside **kwargs.
A positional argument cannot appear after **mapping in a call. Starred iterables are more flexible in the call grammar, but their elements still join the positional argument stream. Read the resulting binding, not just the visual position of a *iterable expression.
Keyword binding
Each explicit keyword argument and entry from **mapping binds by name. A keyword cannot fill a positional-only parameter. A parameter also cannot receive more than one value, whether the collision comes from a positional argument plus a keyword, two unpacked mappings, or an explicit keyword plus a mapping entry.
After named parameters have been matched, **options receives unmatched keywords in a new dictionary. If there is no variadic keyword parameter, any unmatched name raises TypeError. With no unmatched keywords, the parameter is an empty dictionary.
The mapping unpacked with ** must have string keys. A string key does not have to be a valid Python identifier when the callee has a **kwargs parameter, so capture(**{"content-type": "json"}) can work even though capture(content-type="json") cannot be written. Such keys remain accessible only through dictionary operations.
Evaluation and binding are separate
Python evaluates argument expressions before entering the function body. Evaluation follows source order, so side effects in argument expressions can occur before a later duplicate or unexpected keyword makes binding fail. Do not rely on a callee to protect you from side effects already triggered while constructing its call.
Binding then checks the completed positional and keyword inputs against the signature. Missing required parameters, extra positional arguments without a collector, unexpected keywords without a collector, and duplicate values all raise TypeError. The function body does not begin in any of those cases.
Defaults fill eligible parameters that the call did not supply. They do not absorb misspelled keywords. A catch-all **kwargs changes that behavior by accepting the misspelling as a new dictionary entry, which is why flexible signatures need explicit validation.
Containers and object identity
Inside the function, the variadic positional value is a tuple and the variadic keyword value is a dictionary. The tuple cannot be resized, while the dictionary can be edited locally. Those container properties say nothing about the mutability of objects stored inside them.
If a caller supplies a list, both args[0] and the caller still refer to that same list. Mutating it through either reference is visible through the other. Likewise, changing kwargs["headers"]["Accept"] can mutate a caller-owned nested dictionary even though assigning a new top-level key in kwargs does not alter the mapping unpacked by the caller.
The packed containers therefore provide structural separation only at their outer level. Copy nested mutable values when the function needs ownership, or document and test intentional mutation. The stars do not create a deep copy.
Definition order
The full order is positional-only parameters, /, positional-or-keyword parameters, a variadic positional parameter or bare *, keyword-only parameters, and finally a variadic keyword parameter. Not every category is required.
A defaulted positional parameter cannot be followed by a required positional parameter in the same parameter group. Keyword-only parameters do not have that restriction: required and defaulted keyword-only parameters may appear in either order because callers name them.
*args does not mean that every later parameter is also collected. Named parameters after it are keyword-only and bind before unmatched names reach **kwargs. That makes a signature such as def send(*messages, retry=False, **metadata) both flexible and explicit about its own control.
Cooperative method calls
Multiple inheritance sometimes uses **kwargs as a cooperative channel. Each initializer names and consumes the parameters it owns, then calls super().__init__(**kwargs) so the next implementation in the method resolution order can consume its part. The final class in the chain should reject leftovers rather than discard them.
This pattern depends on every participating class following the same contract. One initializer that omits super(), forwards an option twice, or consumes a name owned by another class breaks the chain. It is a protocol for a controlled class hierarchy, not permission to accept arbitrary configuration everywhere.
Prefer keyword-only parameters for cooperative controls because their meaning survives changes in base-class order. Positional forwarding through several unrelated initializers couples every class to one shared slot order and makes refactoring dangerous.
Choosing the flexible boundary
Put a collector at the layer whose variability you understand. An aggregation function may own all of *values; a decorator may transparently forward both streams; an HTTP adapter may own three named controls and reject everything else. These are different contracts even when their implementations all contain a star.
Name a collector after its role when its contents are homogeneous or scoped: *paths, **headers, or **changes. Keep args and kwargs when a wrapper is intentionally neutral about the wrapped callable’s domain. Naming does not enforce a contract, but it tells reviewers which contract to look for.
When the set of options becomes stable, promote them to named parameters. This usually improves documentation and compatibility because adding a new keyword-only parameter does not disturb existing positional calls. Keep **kwargs only if unknown keys remain meaningful after that promotion.
Do not expose flexibility merely because a downstream function has it. The outer API may need a narrower safety, compatibility, or ownership policy. Forwarding is an interface decision, not a mechanical shortcut.
Tests should target the boundary in both directions. Verify the values the callee receives and the errors callers see when they supply unsupported shapes, because either side can drift while the happy path still passes.
Treat the resulting signature as public documentation, even when every current caller lives in the same repository.
Examples
Collect values and name controls
This function accepts a fixed positional order ID, any number of item names, one named control, and open metadata. The domain names communicate more than the conventions args and kwargs would.
def summarize_order(order_id, /, *items, currency="USD", **metadata):
print(f"order: {order_id}")
print(f"items: {items}")
print(f"currency: {currency}")
print(f"metadata: {metadata}")
summarize_order(
"A-17",
"notebook",
"pen",
currency="EUR",
priority=True,
warehouse="west",
)order: A-17
items: ('notebook', 'pen')
currency: EUR
metadata: {'priority': True, 'warehouse': 'west'}order_id cannot be supplied by keyword because it precedes /. The two product names become one tuple. currency binds to its declared keyword-only parameter, so only priority and warehouse remain for metadata.
The insertion order shown for the dictionary follows the keyword order in the call. Treat that order as part of your logic only when the API explicitly defines it; most option handling should select keys by name.
Unpack caller-owned data
Call-side unpacking lets data already held in containers satisfy an explicit signature. Multiple ** expressions are allowed, provided no key is supplied twice.
def schedule(job, owner, /, *, retries=2, urgent=False):
return (
f"job={job}, owner={owner}, "
f"retries={retries}, urgent={urgent}"
)
identity = ("backup", "Mina")
retry_policy = {"retries": 4}
priority = {"urgent": True}
print(schedule(*identity, **retry_policy, **priority))job=backup, owner=Mina, retries=4, urgent=TrueThe tuple elements fill job and owner; the mappings fill keyword-only parameters. schedule() remains strict: an unknown mapping key or a duplicate retries key raises TypeError before its body runs.
That strictness is useful at configuration boundaries. Validate and normalize external data before unpacking it, then let the explicit signature catch drift between the configuration schema and the function contract.
Forward a call without losing metadata
A decorator often cannot spell out every wrapped signature in its runtime implementation. It can collect and forward both argument streams, but it should preserve the wrapped function’s metadata for introspection.
from functools import wraps
from inspect import signature
def trace(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}: args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"returned {result}")
return result
return wrapper
@trace
def quote_price(sku, quantity, /, *, discount=0):
return quantity * (1200 - discount)
print(signature(quote_price))
print(quote_price("BK-7", 2, discount=100))(sku, quantity, /, *, discount=0)
calling quote_price: args=('BK-7', 2), kwargs={'discount': 100}
returned 2200
2200functools.wraps() sets __wrapped__ and copies identifying metadata. inspect.signature() follows __wrapped__ by default, so tools see the public quote_price contract instead of the implementation detail (*args, **kwargs).
Forwarding preserves Python’s binding checks because the wrapped callable still receives the final call. It does not validate the decorator’s logging policy: this toy trace prints values, while production logging must redact credentials and personal data.
Consume owned options before forwarding
An adapter should separate the options it owns from anything it accepts for another layer. This version deliberately has no downstream catch-all, so a typo gets a focused error at the adapter boundary.
def transport(url, /, *, timeout, headers):
return f"GET {url} timeout={timeout} headers={headers}"
def fetch(url, /, **options):
timeout = options.pop("timeout", 5)
headers = options.pop("headers", {})
if options:
unknown = ", ".join(sorted(options))
raise TypeError(f"unknown fetch options: {unknown}")
return transport(url, timeout=timeout, headers=headers)
print(fetch("https://example.test", timeout=2, headers={"Accept": "text/plain"}))
try:
fetch("https://example.test", timeuot=2)
except TypeError as error:
print(error)GET https://example.test timeout=2 headers={'Accept': 'text/plain'}
unknown fetch options: timeuotoptions is already a function-local outer dictionary, so removing top-level keys does not remove them from a mapping used with fetch(**config). The nested headers dictionary is still shared; transport() only reads it here.
For an adapter that truly forwards more options, define an allowlist or document the downstream signature, remove adapter-owned keys with pop(), and forward the remainder once. Decide explicitly whether an adapter option overrides, rejects, or yields to the caller’s value.
Pitfalls
A catch-all hides spelling errors
Fix: prefer a named keyword-only parameter such as *, timeout=5. If a catch-all is necessary, consume recognized keys and reject whatever remains. Test a misspelled option, because happy-path tests do not expose this failure.
This is also an API-evolution problem. A function that accepts every name cannot distinguish a future option from a current typo. Strict boundaries make changes deliberate and keep deprecation paths observable.
Forwarding creates duplicate values
Fix: choose a policy before constructing the call. Reject a caller-supplied value, use setdefault() on a local options dictionary to provide a fallback, or remove the key and intentionally override it. Never rely on the ordering of explicit and unpacked keywords to resolve a collision.
The same rule applies across multiple ** mappings. Dictionary displays such as {**defaults, **overrides} do use later values for duplicate keys, but function calls reject duplicates. Do not transfer the merge semantics of one context to another.
Packed containers are only shallowly separate
Fix: trace identity at the level that can mutate. Copy a nested header dictionary before adding fields, use immutable inputs when practical, or document that the function mutates caller-owned state. Testing only the top-level mapping misses the alias.
Assigning kwargs["processed"] = True does not modify a mapping unpacked into the call. Assigning kwargs["headers"]["X-Trace"] = value can modify the nested headers mapping. Those facts are compatible because the outer and inner identities differ.
Star-unpacking accepts the wrong iterable shape
Fix: pass a scalar without *, and validate container shape before unpacking external data. If a one-shot iterable must be reused, materialize it once at the ownership boundary and make the memory cost explicit.
Length mismatches surface as TypeError only after the iterable has been consumed to construct positional arguments. Avoid call-side unpacking when a collection is conceptually one parameter rather than several slots.
Transparent wrappers are not automatically typed
Fix: type a signature-preserving decorator with ParamSpec and the wrapped return type, and use @wraps for runtime introspection. If the wrapper adds or removes parameters, expose that changed contract instead of claiming perfect transparency.
@wraps does not make an incompatible wrapper callable. A wrapper that inserts a positional value or strips a keyword can still violate the original signature. Exercise positional-only, keyword-only, defaulted, and error cases through the decorated function.
Open keyword forwarding crosses boundaries
Fix: partition options by owner and construct a reviewed mapping for each downstream call. Redact before logging, reject unknown keys at trust boundaries, and test that sensitive or unsupported names cannot travel farther than intended.
Prefixes such as database_timeout can reduce accidental collisions, but nested configuration objects are clearer once several components own distinct settings. One global **kwargs namespace does not scale into a sound configuration model.
Binding edge cases
The call grammar permits several *iterable and **mapping expressions. Each iterable contributes positional values, and each mapping contributes keyword pairs. This enables composable calls, but it does not relax the target signature or duplicate-value rule.
Argument expressions are evaluated from left to right, while starred positional values participate in positional binding. As a result, unusual calls that mix explicit keywords with later starred iterables can be legal but hard to read. Prefer grouping positional material before keyword material even where the grammar accepts another order.
Every ** operand must be a mapping, not merely an iterable of pairs. Its keys must be strings. Duplicate string keys across any keyword sources raise TypeError, including when the target collects unmatched names in **kwargs.
A non-identifier string key is different from a non-string key. capture(**{"content-type": "json"}) can place "content-type" in a variadic keyword dictionary, because the key is a string. capture(**{1: "json"}) raises TypeError, and no direct keyword syntax can spell the hyphenated key.
Positional-only parameters create a deliberate namespace separation. Given def replace(name, /, **changes), the call replace("record", name="display") binds the first value to the positional-only parameter and leaves the keyword name for changes. Without /, the parameter would receive two values and binding would fail.
This pattern is useful for low-level generic APIs, but it can surprise ordinary callers. Use it because the keyword namespace genuinely needs the name, not as a routine workaround for a poorly divided options model.
Errors happen before the body
Python finishes argument evaluation and binding before executing the first statement of the function body. A try inside the callee therefore cannot catch its own missing argument, duplicate value, or unexpected keyword error. The caller or a surrounding wrapper must catch that TypeError if recovery is appropriate.
Avoid broadly catching TypeError around both binding and function execution. The called function may raise TypeError from a bug in its own body, and treating that as a bad signature can hide the defect. inspect.Signature.bind() lets adapters validate a prospective call without running the body.
bind() applies the signature’s binding rules and returns BoundArguments; it raises TypeError when required parameters are missing or values conflict. bind_partial() deliberately permits missing required arguments and suits partial application, not validation of a complete call. apply_defaults() can populate omitted defaults in an existing bound result.
Introspection and typing
inspect.signature(callable) exposes parameter names, kinds, defaults, and annotations. Its parameter kinds correspond to POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD, VAR_POSITIONAL, KEYWORD_ONLY, and VAR_KEYWORD. That vocabulary is more precise than describing everything before **kwargs as “normal arguments.”
By default, inspect.signature() follows a __wrapped__ chain created by functools.wraps(). This is why the decorator example reports the original signature. Custom decorators that change the public call contract may need an explicit __signature__, but such metadata must agree with what the wrapper actually accepts.
An annotation on *values: int describes each collected positional value as an int, not the runtime tuple as a whole. Similarly, **labels: str describes each keyword value as a str; the keys are inherently strings at a valid call boundary. Ordinary Python execution does not enforce either annotation.
When a finite set of keyword names has different value types, type **kwargs as Unpack[SomeTypedDict]. A type checker can then reason about required keys, optional keys, value types, and unexpected names. The runtime still receives a dictionary and still needs validation for untrusted data.
For transparent higher-order functions, a ParamSpec captures the positional and keyword portions of a callable’s static signature. Annotating the wrapper with *args: P.args and **kwargs: P.kwargs, then returning Callable[P, R], preserves the relationship between the wrapped callable and its callers. It does not replace @wraps, which serves runtime metadata.
Typing cannot rescue an intentionally vague interface. If an adapter accepts arbitrary keys but forwards only a subset, express the accepted schema directly when possible. Static precision and runtime rejection should describe the same ownership boundary.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug