Back to Blog
Python

Python Star Operator Unpacking: Syntax and Use Cases

python star operator unpacking: Learn how Python's * and ** operators unpack iterables and mappings in function calls, definitions, assignments, and collection literals.

pythonunpackingstar-operatorargs-kwargsiterables
Illustration of a Python star operator spreading values from a list into separate slots on the right

Python's star operator (*) and its double-star variant (**) control how values move between collections, function calls, and function signatures. The python star operator unpacking behavior appears in four distinct contexts: expanding iterables into function arguments, collecting arguments in a function definition, assigning multiple values from a sequence, and merging collection literals. Each context has its own rules, so recognizing which one you are in is the first step to using the operator correctly.

Expanding Iterables in Function Calls

A single star before an iterable inside a function call expands that iterable into positional arguments. This is the most common use of unpacking in everyday code.

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

The iterable must produce exactly the number of arguments the function expects, unless the function itself accepts a variable number of arguments. If values contains two or four elements, add(*values) raises a TypeError because the argument count does not match the signature.

The double-star form expands a mapping into keyword arguments. The mapping's keys must be strings that match the parameter names.

def describe(name, age, city): return f"{name}, age {age}, from {city}" info = {"name": "Ada", "age": 36, "city": "London"} print(describe(**info)) # Ada, age 36, from London

A TypeError is raised when a key does not match any parameter name, or when a required parameter is missing. You can combine both forms in one call, but the ** mapping must come after the * iterable and any explicit positional arguments.

def request(method, url, timeout=30, retries=0): return f"{method} {url} (timeout={timeout}, retries={retries})" params = {"timeout": 10} print(request("GET", "https://api.example.com", **params))

Collecting Arguments in Function Definitions

The same symbols reverse their role in a function signature. A single star collects extra positional arguments into a tuple, and a double star collects extra keyword arguments into a dictionary.

def log(level, *messages): for message in messages: print(f"[{level}] {message}") log("INFO", "started", "processing", "finished")

Here *messages receives ("started", "processing", "finished") as a tuple. The function can be called with any number of positional arguments after level, including zero.

The double-star form collects keyword arguments that were not matched by named parameters.

def configure(base_url, **options): print(f"Base URL: {base_url}") for key, value in options.items(): print(f"{key}={value}") configure("https://api.example.com", timeout=30, retries=3)

**options becomes {"timeout": 30, "retries": 3}. This pattern is common in wrappers, decorators, and configuration functions where the set of accepted options is intentionally open-ended.

A bare * in a signature marks the boundary between positional and keyword-only parameters. Parameters after the * cannot be passed positionally.

def connect(host, port, *, ssl=True): return f"{host}:{port} ssl={ssl}" connect("db.example.com", 5432, ssl=False) # valid connect("db.example.com", 5432, False) # TypeError: takes 2 positional arguments

Extended Unpacking in Assignments

The star operator also appears on the left side of an assignment, where it collects the remaining values from an iterable into a list.

first, *middle, last = [1, 2, 3, 4, 5] print(first) # 1 print(middle) # [2, 3, 4] print(last) # 5

The starred target always receives a list, even when the source is a tuple, string, or other sequence. Only one starred target is allowed per assignment level.

head, *tail = "python" print(head) # 'p' print(tail) # ['y', 't', 'h', 'o', 'n']

This works with any iterable, including generators. When the iterable is exhausted, the starred target receives an empty list.

first, *rest = [42] print(first) # 42 print(rest) # []

Extended unpacking is useful for splitting a sequence into a leading element and the remainder, which is a common pattern in recursive algorithms and data-processing pipelines.

Merging Collection Literals with Star Unpacking

Star unpacking inside list, set, and dictionary literals provides a concise way to combine collections without calling methods like extend or update.

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

The same syntax works for sets, producing a union of the elements.

s1 = {1, 2} s2 = {2, 3} union = {*s1, *s2} print(union) # {1, 2, 3}

For dictionaries, the double-star form merges key-value pairs into a new dictionary. Later keys overwrite earlier ones when they collide.

defaults = {"timeout": 30, "retries": 0} overrides = {"timeout": 10} config = {**defaults, **overrides} print(config) # {'timeout': 10, 'retries': 0}

This creates a new dictionary and leaves the source dictionaries unchanged. It is a common way to apply defaults while allowing callers to override specific values.

Common Mistakes and Edge Cases

Unpacking fails when the source is not iterable. Passing an integer to * raises TypeError: 'int' object is not iterable. The same applies to **, which requires a mapping; passing a list raises TypeError: argument of type 'list' is not a mapping.

In function calls, you can use * multiple times, but the expanded positional arguments are placed in order. You cannot place a ** expansion before a * expansion or before explicit positional arguments.

def f(a, b, c): return a + b + c f(*[1], *[2, 3]) # valid, returns 6

In assignments, a starred target cannot be used alone. *rest = [1, 2, 3] is a syntax error; the starred target must appear alongside at least one unstarred target.

Another subtle point: unpacking a generator consumes it. If you unpack the same generator twice, the second unpacking produces nothing because the generator is already exhausted. This matters when you pass a generator to a function that unpacks it internally.

Performance and Memory Behavior

Unpacking materializes the iterable it consumes. When you write [*large_generator], the entire generator is consumed and stored in a new list, so memory usage grows with the number of elements. The same is true for {**a, **b}, which builds a new dictionary containing every key from both sources.

For typical argument counts, the cost of *args collection is negligible. A function called with a handful of arguments creates a small tuple, and the overhead is not a practical concern. The same applies to **kwargs, which builds a dictionary of the supplied keyword arguments.

The more relevant cost appears when unpacking is used to merge large collections. [*a, *b] creates a new list and copies every element from both sources. If you only need to iterate over the combined elements, itertools.chain(a, b) avoids the copy and the associated memory allocation. Choose the unpacking form when you actually need a materialized collection, and choose chain or similar lazy approaches when you only need to consume the elements once.

Maintainability: Choosing the Right Unpacking Pattern

*args and **kwargs make a function signature less explicit. Use them when the argument set is genuinely variable, such as in decorators, logging wrappers, or functions that forward arguments to another callable. For functions with a fixed, known set of parameters, explicit named parameters are clearer and let static analysis tools and IDEs provide better autocompletion and type checking.

Extended unpacking in assignments usually improves readability because it expresses the "first and rest" or "first, middle, last" pattern directly. It replaces index arithmetic that is harder to read and more error-prone.

# Clearer first, *rest = items # Equivalent but more error-prone first = items[0] rest = items[1:]

Merging dictionaries with {**a, **b} is concise and non-mutating, which makes it a good default when you need a combined mapping. If you are repeatedly merging many dictionaries in a loop, consider whether building a single dictionary incrementally with update is more efficient and easier to reason about.

The star operator is not a single feature but a family of related syntax rules. Keeping the four contexts distinct in your mental model — call expansion, signature collection, assignment unpacking, and literal merging — prevents most of the confusion that surrounds python star operator unpacking in practice.

python star operator unpacking: Practical Usage and Code Exa | RYUSLOG DEV