AlgoMaster Logo

*args and **kwargs

High Priority24 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

Some functions can't know in advance how many inputs they'll get. A cart can hold 1 item or 50. A search query can include 0 filters or a dozen. A log call might attach 3 fields to one event and 8 to another. *args and **kwargs are the two pieces of syntax Python gives you for accepting an unknown number of arguments cleanly. This lesson covers what each one does, how they appear in a signature together, and the unpacking syntax on the call side.

Why Variadic Parameters Exist

The functions you've written so far have a fixed shape: a function with three parameters takes three arguments, no more and no less. Most real-world functions fit that shape, but some don't. Built-ins like print, min, max, and dict accept any number of arguments because the question they answer (print these things, find the smallest, build a mapping) doesn't have a fixed-size answer.

Consider logging a cart event. Each event has a type, and then anywhere from one to many items attached to it:

Fine for a single item. But what if a customer adds three items at once? You'd have to call log_event_v1 three times, or rewrite the function to take a list:

The list works, but the call site is now noisy: every call has to wrap items in [ ... ], even when there's only one. *args lets the function itself collect any extra positional arguments into a tuple, so callers write the items inline:

Three calls, three different argument counts (3, 1, 0), one function signature. No wrapping at the call site. The third call passes zero items, which is also fine: items is just an empty tuple inside the function, and the for loop does nothing.

The same idea applies to keyword arguments. A search-by-filter function might accept any combination of category, min_price, max_price, in_stock, on_sale, and so on, with most calls using only two or three of them. Writing each combination as its own function would be silly. **kwargs lets one function accept any keyword arguments at all, collect them into a dict, and decide what to do with them.

This is the whole motivation: when the count of inputs isn't fixed, the function signature shouldn't pretend it is.

*args Basics

*args collects extra positional arguments into a tuple. The asterisk is the actual Python syntax; the name args is a convention. You can call the parameter anything, and the language won't care:

Three things to notice. First, items is a tuple, not a list. Tuples are immutable, which signals "the function received these inputs; it shouldn't mutate them". Second, the tuple contains only the extra positional arguments, the ones beyond what regular parameters absorbed. The first argument went to event_type; everything after it went into items. Third, the name on the left of the function definition is items, not *items. The asterisk only appears in the parameter list; inside the function body, you refer to the parameter by its plain name.

Most Python code uses the name args by convention when the parameter has no domain-specific meaning. When it has one, use a meaningful name like items, tags, or numbers. The convention is *args, but the meaning lives in whatever name you choose.

Three calls with three different counts. The empty call returns 0 because sum of an empty tuple is 0. There's no magic happening in cart_total: it just receives a tuple of prices and hands it to sum.

A common confusion is what happens if you pass a list directly:

The call passed one positional argument (the list), so prices became ([29.99, 14.99, 9.99],), a tuple containing one list. sum then tried to add 0 + [29.99, 14.99, 9.99] and failed. To spread the list across the parameters, you have to unpack it at the call site with *. The reason args is a tuple and not a list is partly historical and partly intentional. Tuples are immutable, which makes them safer to share. Inside the function, you can iterate, index, or unpack the tuple like any other tuple, but you can't accidentally .append to it and surprise the caller. If you need a list, convert explicitly with list(args).

**kwargs Basics

**kwargs collects extra **keyword** arguments into a dictionary. The double-asterisk is the syntax; the name kwargs (short for "keyword arguments") is convention. Inside the function, the parameter is just a regular dict.

Each name=value pair at the call site becomes a key-value pair in the dict. The keys are strings (the parameter names you used at the call), and the values are whatever was passed in. The order of the entries matches the order they were passed, because Python dicts preserve insertion order.

A search function that supports many optional filters is a natural fit:

The function accepts any keyword arguments at all. Inside, filters is just a dict, so the rest of the logic is normal dict code. If you wanted to enforce a fixed set of allowed filter names, you'd validate the keys at the top of the function, but the collection of arguments is unconstrained.

A common mistake is mixing up the syntax for collecting and the syntax for unpacking. The double asterisk in the parameter list collects keyword arguments into a dict. The same syntax at a call site unpacks a dict into keyword arguments. The placement (parameter list vs. call) is what tells Python which direction the data is flowing.

Two different call shapes, same dictionary inside the function. We'll come back to the unpacking direction in the call-site section below.

Keys in **kwargs are always strings, because Python parameter names are identifiers. You can't pass build_query(min-price=10) because min-price isn't a valid name. The fix is to call with **{"min-price": 10}, which is occasionally useful when the keys come from external data (a JSON payload, a config file) that uses names Python doesn't accept directly. Even then, you have to look up the values inside the function with filters["min-price"] because you can't write filters.min-price either.

Order in a Signature

A function signature can mix regular parameters, *args, and **kwargs together, but the order is fixed. From left to right:

  1. Regular positional or keyword parameters.
  2. *args (collects remaining positional arguments).
  3. Keyword-only parameters (anything after *args must be passed by name).
  4. **kwargs (collects remaining keyword arguments).

The function below uses three of those slots:

Three groups, three destinations. The first positional argument matches customer. The next three positional arguments are extras, so they end up in cart_items. The two keyword arguments don't match any regular parameter name, so they end up in options. If the call had passed a regular parameter by name (customer="alice"), it would still go to customer instead of options, because Python checks named parameters before falling through to **kwargs.

The diagram shows how the three groups split out of a single call. The first positional argument has a named home (customer); everything else falls through to one of the two catch-all parameters depending on whether it was passed positionally or by name.

Anything between *args and **kwargs becomes a **keyword-only** parameter. That means it can't be passed positionally, even if it has no default value. This is how libraries enforce "pass this argument by name so the call site is readable":

delivery sits after *cart_items, so it can only be passed by name. The call works. Trying to pass it positionally fails:

The interpreter sees three positional arguments. The first one fills customer. The next two become extra positionals and go into cart_items. delivery never gets a value, and Python complains. The fix is to call with delivery="express", which is the whole point of keyword-only parameters.

Reversing the order doesn't work. **kwargs has to come last, and *args has to come before keyword-only parameters. The interpreter rejects out-of-order signatures at definition time:

The error happens before the function ever runs, because the signature itself is invalid. There's only one legal order, and you don't have to memorize it deeply; the language enforces it for you.

PositionWhat goes hereExample
1Regular parameters (positional or keyword)customer, event_type
2*args (catches extra positionals)*cart_items, *prices
3Keyword-only parameters (require name=value at call)delivery, currency
4**kwargs (catches extra keywords)**options, **filters

Unpacking at the Call Site

The asterisk and double-asterisk also appear on the call side, where they do the opposite of what they do in a signature. In a signature, * collects positional arguments into a tuple; at a call, * unpacks an iterable into positional arguments. Same direction in the table below, opposite jobs depending on where they sit.

That's almost certainly not what we wanted. The function printed one "item" that happens to be a list. To spread the list across the parameters, prefix it with *:

The *cart on the call side is equivalent to writing "Wireless Mouse", "USB Cable", "HDMI Cable" by hand. Each element of the iterable becomes its own positional argument. This works with any iterable: lists, tuples, sets, generators, even strings (which would unpack to individual characters, usually not what you want).

The same logic applies to ** for dictionaries. A **dict_value at the call site unpacks the dict into keyword arguments.

Without the **, you'd be passing one positional argument (a dict), and the function would either reject it (no positional parameter exists) or treat it as a single value. With **, the dict's keys become parameter names and the dict's values become argument values, exactly as if you'd typed each name=value by hand.

Unpacking shines when you have data in a structured form and want to feed it to a function that takes named parameters. You can mix unpacked values with explicit ones:

Two unpacking sources are allowed, in any order, alongside explicit arguments. The first call uses one positional unpacking and one keyword unpacking. The second call mixes an explicit positional ("HDMI Cable"), a positional unpacking (*cart), an explicit keyword (gift_wrap=False), and a keyword unpacking (**{"delivery": "standard"}).

There's one rule that catches people: you can't unpack the same key twice into **kwargs. If two sources try to provide the same keyword, Python raises TypeError.

Both dicts try to bind category, and Python doesn't pick one silently. If you want the second to win, merge first: {**base, **override} produces a single dict where later keys overwrite earlier ones, and you can unpack that.

The same conflict rule applies to explicit keyword arguments versus unpacking: you can't pass category="Foo" and **{"category": "Bar"} in the same call, because both want to bind category.

Patterns and Pitfalls

A handful of patterns show up repeatedly in real code. They all reduce to the basics above, but seeing them together makes the shapes recognizable.

Pass-Through to Another Function

A common use of *args and **kwargs is forwarding all arguments to another function without caring what they are. The outer function adds something (logging, caching, validation), then delegates the actual work.

time_call doesn't know or care what cart_total's real signature looks like. It collects all the extras into args and kwargs, then unpacks them back when calling func. Whatever shape the inner function expects, the wrapper passes through unchanged. This is the same shape you'll see in a decorator (decorators wrap a function with another function); the pattern is "collect on the way in, unpack on the way out".

Building Calls Programmatically

When the arguments come from data, *args and **kwargs let you assemble a call from pieces.

The call site reads like an assembly: take this customer, spread these items, apply these options. Without unpacking, you'd be writing a long place_order(who, what[0], what[1], delivery=how["delivery"], gift_wrap=how["gift_wrap"]) that breaks the moment the data changes shape.

Common Mistake: Unpacking Where a Single Value Is Required

If a function expects a single positional argument and you unpack a list with one item, things look the same on the surface but the meaning is different.

*cart_two produced two positional arguments, on top of "alice", for a total of three. The function only accepts two. If you intended to pass the list as a single value, drop the asterisk: add_to_wishlist("alice", cart_two). The asterisk should only appear when the function actually has room for a variable count of positionals.

Common Mistake: Passing **dict to a Function Without **kwargs

Unpacking a dict into a function that doesn't accept arbitrary keyword arguments works fine as long as every key in the dict matches a real parameter. Surprises happen when the dict has keys that the function doesn't recognize.

There are two fixes. One: filter the dict to only the keys the function accepts before unpacking. Two: change the function to accept **kwargs and ignore the extras. Which is right depends on whether extra keys should be silently dropped (filter) or whether the function should tolerate any incoming shape (kwargs).

Common Mistake: Forgetting the Single Asterisk Inside Collected args

Inside a function, args is a tuple, not a list of bare values. To call another function with the same arguments, you have to unpack it on the way out, otherwise you're passing the tuple itself.

prices inside shipping_quote is a tuple. Passing it to cart_total without unpacking sends the whole tuple as one positional argument, which cart_total collects into its own prices parameter as a one-element tuple containing a tuple. sum then can't add an integer and a tuple. The fix is the same * we used earlier:

The pattern collect-with-*, pass-through-with-* is the rule. It looks symmetrical because it is: the asterisk is the gate the data passes through in both directions.

Quiz

*args and **kwargs Quiz

10 quizzes