Back to Blog
Python

Python Starred Expression: Unpacking and Expansion

python starred expression: Learn how Python starred expressions work in assignments, function calls, and definitions, with practical examples and common pitfalls.

Pythonunpacking*args**kwargsfunction argumentsiterables
Illustration of Python starred expression unpacking a list into multiple variables and function arguments.

A Python starred expression is the * or ** syntax that appears in assignments, function definitions, and function calls. It controls how iterables and mappings are unpacked or collected. This article covers the core behavior, practical use cases, and the mistakes that trip up developers when they first use starred expressions.

The Core Behavior of a Starred Expression

In Python, a single asterisk * before a variable name unpacks an iterable into its individual elements. A double asterisk ** unpacks a mapping into keyword arguments. The behavior depends on the context.

For example, when you assign a list to multiple variables, you can use a starred target to capture the remaining elements:

first, *rest = [1, 2, 3, 4] print(first) # 1 print(rest) # [2, 3, 4]

Here *rest collects all elements after the first into a list. This is called extended unpacking. The starred target can appear in any position, but only one starred target is allowed per assignment.

The same syntax works with tuples, strings, and any iterable:

head, *tail = "hello" print(head) # 'h' print(tail) # ['e', 'l', 'l', 'o']

The result of a starred target is always a list, even if the source is a string or a tuple.

Extended Unpacking in Assignment Statements

Extended unpacking is not limited to the left side of a simple assignment. You can use it in for loops and in multiple assignment statements.

for first, *rest in [(1, 2, 3), (4, 5)]: print(first, rest)

This prints:

1 [2, 3]
4 [5]

When the iterable has fewer items than the non-starred targets require, a ValueError is raised. For example:

a, *b, c = [1, 2]

This raises ValueError: not enough values to unpack (expected at least 2, got 2) because a and c need at least two elements, but the list has exactly two, leaving nothing for b. In practice, the starred target can be empty, but the fixed targets must be satisfied.

You can also use a starred expression in the middle:

a, *middle, z = [1, 2, 3, 4, 5] print(a, middle, z) # 1 [2, 3, 4] 5

This is useful for ignoring parts of a sequence you don't need, such as the first and last elements.

Collecting Variable-Length Arguments in Function Definitions

The most common use of starred expressions is in function definitions. A single *args parameter collects positional arguments into a tuple, and a **kwargs parameter collects keyword arguments into a dictionary.

def log(message, *args, **kwargs): print(message) print("args:", args) print("kwargs:", kwargs) log("start", 1, 2, level="info")

Output:

start
args: (1, 2)
kwargs: {'level': 'info'}

The names args and kwargs are conventional but not required. The important part is the asterisk. A function can use *args without **kwargs, and vice versa.

A bare * in a function definition forces all following parameters to be keyword-only. This is a separate feature but often appears alongside starred expressions.

def compare(a, b, *, key=None): pass

Here key must be passed as a keyword argument, not positionally.

Expanding Iterables and Mappings in Function Calls

The same * and ** syntax works in function calls to expand an iterable or mapping into separate arguments.

def add(a, b, c): return a + b + c values = [1, 2, 3] print(add(*values)) # 6

The list values is unpacked positionally into a, b, and c. The number of elements must match the function's positional parameters, otherwise a TypeError is raised.

For keyword arguments, use ** with a dictionary:

def describe(name, age): print(f"{name} is {age} years old") person = {"name": "Ada", "age": 36} describe(**person)

The dictionary keys must match the parameter names exactly.

You can combine both forms in one call:

def make_point(x, y, z=0): return (x, y, z) coords = [1, 2] point = make_point(*coords, z=3)

The *coords provides positional arguments, and z is passed as a keyword.

Starred Expressions in List and Tuple Contexts

Starred expressions are also allowed inside list, tuple, and set literals. This lets you merge multiple iterables without calling extend or concatenating.

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

The same works with tuples and sets:

t1 = (1, 2) t2 = (3, 4) merged = (*t1, *t2) print(merged) # (1, 2, 3, 4)

For dictionaries, use ** to merge them:

d1 = {"a": 1} d2 = {"b": 2} merged = {**d1, **d2} print(merged) # {'a': 1, 'b': 2}

If keys overlap, later dictionaries override earlier ones. This is a concise way to combine dictionaries without mutating the originals.

Common Mistakes and Unexpected Behavior

One frequent mistake is using a starred expression in a context where it is not allowed, such as in a slice or a function call with mismatched lengths.

def add(a, b, c): return a + b + c nums = [1, 2] print(add(*nums)) # TypeError: add() missing 1 required positional argument: 'c'

Another issue is forgetting that a starred target always produces a list, not the original type. If you need a tuple, convert it explicitly.

Also, you cannot use more than one starred expression in a single assignment target:

*a, *b = [1, 2, 3] # SyntaxError

The same restriction applies in function calls: you can use only one * expansion and one ** expansion per call, though you can mix them.

When merging dictionaries with **, the result is a new dictionary. If you need to update an existing dictionary in place, use update() instead, because {**d1, **d2} creates a copy.

Performance and Readability Tradeoffs

Starred expressions are implemented in the interpreter and are generally fast. The main cost is the creation of a new list or tuple when unpacking. For large iterables, that allocation is unavoidable, but it is usually negligible compared to the cost of the operation itself.

The bigger concern is readability. Overusing *args and **kwargs can make function signatures unclear, because the reader cannot see what arguments are expected. Use them when the number of arguments is genuinely variable, such as in decorators, wrappers, or logging utilities. For fixed-argument functions, explicit parameters are clearer.

Extended unpacking in assignments improves readability when you need to ignore part of a sequence, but it can obscure intent if the pattern is complex. For example, _, *rest = data is a common idiom to skip the first element, but it may be clearer to write rest = data[1:] if you only need the tail.

In performance-sensitive code, avoid repeated unpacking of the same large iterable inside a loop. If you need to unpack once, do it outside the loop. The interpreter handles starred expressions efficiently, but the resulting list allocation still has a cost.

Using Starred Expressions with Generators and Iterators

A starred expression can unpack any iterable, including generators. This is useful when you need to materialize a generator into a list or pass its elements to a function.

def generate_numbers(): yield 1 yield 2 yield 3 a, *rest = generate_numbers() print(a, rest) # 1 [2, 3]

Be careful when unpacking a generator that produces a large or infinite sequence. Unpacking consumes the entire generator into memory, which can cause high memory usage. For finite generators, this is often acceptable, but for streaming data, consider using itertools.islice or a loop instead.

When you use * in a function call with a generator, Python expands it lazily? No, it does not. The generator is fully consumed to create the argument list. So the same memory concern applies.

Compatibility and Version Considerations

Extended unpacking and starred expressions in list/set/dict literals are supported in Python 3.5 and later. The * in function definitions has been available since Python 3.0, and **kwargs even earlier. If you need to support Python 2, starred expressions are not available, but that is rarely a concern for modern projects.

One subtle change: in Python 3.5, the syntax {**d1, **d2} was introduced for dictionary merging. Earlier versions required dict(d1, **d2) or manual updates. If you are working with older codebases, check the Python version before relying on this syntax.

Another version-specific detail is that starred expressions in tuple literals were also added in Python 3.5. Before that, you had to use tuple([*a, *b]) or concatenation.

python starred expression: Practical Usage and Code Examples | RYUSLOG DEV