backend / python core / tricky questions / 19_starred_unpacking.md

Starred unpacking

2 min read source

Starred unpacking

The gotcha

* and ** in assignment, function calls, and (since 3.5) literals do different things in different positions. A few subtle rules trip people up.

Minimal repro

Assignment

a, *b, c = [1, 2, 3, 4, 5]
print(a, b, c)   # 1 [2, 3, 4] 5

# `*b` is always a list — even if it captures one element or zero:
a, *b = [1]
print(b)   # []   (not None)

a, *b, c = [1, 2]
print(b)   # []   (a=1, c=2)

Function definitions

def f(a, *b, c, **d):
    # *b: variable positional → tuple
    # c is keyword-only because it comes after *b
    # **d: variable keyword → dict
    ...

f(1, 2, 3, c=10, x=1, y=2)
# a=1, b=(2, 3), c=10, d={'x': 1, 'y': 2}

Function calls

def f(a, b, c): ...

args = [1, 2, 3]
f(*args)            # spreads: f(1, 2, 3)

kwargs = {"a": 1, "b": 2, "c": 3}
f(**kwargs)         # spreads as keywords

# Combining:
f(1, *[2], **{"c": 3})   # legal

Literal merging (3.5+)

[*a, *b]           # concat lists into a new list
{*a, *b}           # union of sets
{**a, **b}         # merge dicts (right wins on key conflict)
(*a, *b)           # tuple of concatenated

Forbidden / surprising

# Multiple stars in target — illegal:
*a, *b = [1, 2, 3]   # SyntaxError: multiple starred expressions in assignment

# Star without context — illegal:
*a = [1, 2, 3]   # SyntaxError

# Star in dict literal must be **:
{*a: 1}   # SyntaxError, you'd need {**other_dict, "a": 1}

**kwargs consumes only str keys (when calling)

def f(**kw): ...
f(**{"a": 1})    # ok
f(**{1: "x"})    # TypeError: keywords must be strings

Why it matters

Used heavily in: function wrappers (def wrap(*args, **kwargs)), partial application, dict merging in factories, splatting tuples into iterables.

How to avoid mistakes

  • Remember: * in target always produces a list; in function call it spreads.
  • Remember: **kwargs is a dict in the function, but a spread when calling.
  • Keyword-only args after * are a feature — use them to force named arguments:
def fetch(url, *, timeout=10, retries=3):
    # caller MUST write fetch("...", timeout=5)
    ...

Interview angle

“What does a, *b, c = range(2) do?” (ValueError: not enough values to unpack — needs at least 2 values, but *b requires at least 0, so this needs at least 2: a and c). Or “What’s the difference between def f(a, b) and def f(a, *, b)?”