skip to content

In Python, what do `*` and `**` do at a call site and inside list or dict literals?

level: middleimportance: should knowfreq 52%

answer

  1. One direction collects, the other spreads
  2. The same syntax means different things in two contexts
  3. Duplicates are fatal in one place, benign in the other
  4. Key types are restricted only at a call
  5. A tuple display still needs its comma

basics

~20 s

At a call site * spreads an iterable into positional arguments and ** spreads a mapping into keyword arguments. Inside a literal they splice contents in: [*a, *b] concatenates, {*a, *b} unions, {**m1, **m2} merges with later keys winning.

solid answer

~40 s

These are the *spread* direction of unpacking, generalised by PEP 448 in Python 3.5. At a call, `f(*seq)` passes each item of any iterable as a positional argument and `f(**mapping)` passes each entry as a keyword argument; since 3.5 you may use several of each and mix them with ordinary arguments, evaluated left to right. Inside a display, `[*a, *b]`, `(*a, *b)`, `{*a, *b}` and `{**m1, **m2}` splice the contents of any iterable or mapping into the new object. The two contexts differ on duplicates and key types: a dict literal quietly lets the later key win, while a call raises `TypeError` for a repeated keyword and requires the keys to be strings. A bare `(*a)` is a `SyntaxError` — a tuple display needs the comma, as in `(*a,)`.

code

python · 6 lines
python
def triage(kind, priority, *, owner="unassigned"):
    return f"{kind}/{priority} -> {owner}"

args = ("bug", "high")
extra = {"owner": "queue-a"}
print(triage(*args, **extra))  # bug/high -> queue-a

go deeper

for a junior

Recall what the two symbols do at a call: one spreads a sequence into positional arguments, the other spreads a mapping into keyword arguments. Being able to read f(*args, **kwargs) at a call site is the bar here.

for a middle

Explain that several of each are allowed since 3.5 and that displays can splice too. The detail that earns the point is the duplicate-key asymmetry: last-wins in a dict display, TypeError at a call.

for a senior

Show judgement about where the spread form helps and where it hides a contract. Splatting a mapping into a call couples the caller to parameter names; say when you would build an explicit argument list instead, and note the eager consumption of a generator source.

for a principal

Own the API-shape consequence: functions whose callers routinely splat a config dict have effectively made their parameter names a public schema. Decide whether that boundary should take a structured object instead, and what that costs at the seams.

### Two directions of the same idea Unpacking on the left of `=` *collects*: `first, *rest = xs`. A star in a call or a display does the opposite — it *spreads* an existing iterable or mapping into a position where individual items are expected. PEP 448, "Additional Unpacking Generalizations", landed in Python 3.5 and is what makes the spread form as flexible as it now is; before 3.5 a call could carry only one `*` and one `**`, always last, and displays could not use them at all. ### At a call site `f(*iterable)` feeds each item of the iterable as a separate positional argument, in order. The source can be any iterable, not just a tuple or list — `f(*range(3))` and `f(*"ab")` are fine. `f(**mapping)` feeds each entry as a keyword argument. Since 3.5 you can combine them freely: `g(*[1], *[2, 3])`, `g(*[1, 2], 3)` and `h(**defaults, **overrides)` are all legal, and arguments are evaluated strictly left to right, which is what makes "defaults first, overrides second" a working idiom. Two rules bite here and nowhere else: - **Keyword keys must be strings.** `f(**{1: "x"})` raises `TypeError: keywords must be strings`. A dict *display* has no such rule — `{**{1: "x"}}` is a perfectly good dict — so the restriction belongs to the call, not to `**`. - **A duplicate keyword is an error.** `f(**{"a": 1}, **{"a": 2})` raises `TypeError`, saying the function got multiple values for keyword argument `a`. The same duplication inside a dict display is silently allowed, with the later key winning. That asymmetry is the single most useful thing to know about this syntax, and it is a favourite interview probe: merging two config mappings with `{**base, **overrides}` is a deliberate last-wins merge, while splatting the same two mappings into one call is a hard error. If you want last-wins semantics at a call, merge first and splat once. The `**` source does not have to be a `dict` either. Anything with `keys()` and `__getitem__` — any mapping — can be spread, which is how mapping-like configuration objects get passed straight into a function. ### Inside a display The same operators splice contents into a new container: - `[*a, *b]` builds a list from any two iterables — a concatenation that does not care whether the sources are lists, tuples, sets or generators. - `(*a, *b)` builds a tuple. Note the comma requirement: `(*a,)` is a tuple of a's items, while a bare `(*a)` is `SyntaxError: cannot use starred expression here`, because parentheses alone are grouping, not a tuple display. - `{*a, *b}` builds a set — effectively a union, deduplicating as sets do. Spreading a dict here spreads its **keys**, since iterating a dict yields keys. - `{**m1, **m2}` builds a dict, later entries overwriting earlier ones. You can mix literal entries in: `{**base, "retries": 3}` is a copy-with-override in one expression. Because a display materialises a new object, `[*gen]` fully consumes a generator. `[*a]` is close in effect to calling `list(a)`; the display form's advantage is that it composes — you can interleave several sources and literal elements in one expression. ### Reading it correctly The star in these positions is a syntactic form, not an operator you can apply to an arbitrary expression: it is legal only in a call's argument list, in a display, and in an assignment target list. Since 3.8 it is also allowed unparenthesised in `return` and `yield` statements. Everywhere else you get a `SyntaxError`. Two things this syntax is *not*. It is not the declaration side — a star in a function's parameter list is a different feature with different rules, and the two are frequently confused in interviews because they look identical. And it is not a copy operator: `[*a]` builds a new list holding the *same* element objects, so mutating an element still shows through both containers. All of this is stable from 3.5 through 3.14; the only later change in this area was allowing an unparenthesised starred expression in `return` and `yield` in 3.8.

  • Why does `{**m1, **m2}` accept a repeated key while passing the same two mappings into one call raises TypeError?
    A dict display is defined as building entries in order, so a repeated key simply overwrites — last wins. A call binds each keyword to a distinct parameter, so a repeated keyword is genuinely ambiguous and raises `TypeError: got multiple values for keyword argument`. If you want last-wins behaviour at a call, merge the mappings into one dict first and spread that.
  • What is the difference between `(*a)` and `(*a,)` in Python?
    `(*a,)` is a tuple display containing the items of `a`. `(*a)` is a `SyntaxError` — the parentheses there are grouping, not a tuple constructor, and a starred expression is not a valid standalone expression. The comma is what makes it a tuple, exactly as in any other single-element tuple.
  • Does the `**` source at a call site have to be a dict?
    No. Any mapping works — anything providing `keys()` and `__getitem__` can be spread into keyword arguments, and Python builds the keyword dictionary from those. The only hard constraint is that the keys it yields must be strings; otherwise the call raises `TypeError: keywords must be strings`.

saying these in an interview costs you the question

  • Says a call may contain only one star and one double star
  • Thinks a dict display raises on a duplicated key
  • Claims non-string keys are acceptable as keyword arguments
  • Believes `[*a]` deep-copies the elements it splices
  • Says `(*a)` builds a tuple without the trailing comma
  • Confuses the spread form with a star in a parameter list

context