Free lesson · Python Basic to Advance
Argument Unpacking
Argument Unpacking
What Argument Unpacking Is
Argument unpacking is the dual of *args and **kwargs (Topic 9.4). Where *args collects positional arguments into a tuple, *sequence at a call site unpacks a sequence into individual positional arguments. Where **kwargs collects keyword arguments into a dict, **mapping unpacks a dict into individual keyword arguments.
The relationship is symmetric:
- In a function definition:
*and**collect - In a function call:
*and**distribute
This duality is one of Python's most powerful argument-handling features. It enables passing iterables as positional arguments, dicts as keyword arguments, and forwarding all arguments from a wrapper.
Interactive animation: One star in a definition collects arguments into a tuple, and the same star at a call spreads them out.
Unpacking a List or Tuple With *
The * operator at a call site spreads an iterable across positional parameters:
def add(a, b, c):
return a + b + c
values = [1, 2, 3]
print(add(*values))
# 6
This code unpacks a list into the three positional parameters of add. The star operator spreads each element of values into the corresponding parameter. Add receives a equals one, b equals two, c equals three.
The iterable's length must match the parameter count, just as with add(1, 2, 3):
def add(a, b, c):
return a + b + c
print(add(*[1, 2]))
# TypeError: add() missing 1 required positional argument: 'c'
print(add(*[1, 2, 3, 4]))
# TypeError: add() takes 3 positional arguments but 4 were given
This code shows that unpacking does not change argument-count rules. Too few elements produces a missing-argument error. Too many produces a too-many error. The star is just a way to spread an iterable, not to relax argument matching.
Any iterable works - lists, tuples, sets, generators, strings (character by character):
def print_three(a, b, c):
print(a, b, c)
print_three(*"abc")
# a b c
print_three(*range(3))
# 0 1 2
print_three(*(10, 20, 30))
# 10 20 30
This code shows unpacking from a string, a range, and a tuple. Strings unpack character by character. Range produces integers in order. Each iterable type works the same way once it is iterated.
Interactive animation: Passing coords to point fails as one argument, *coords passes three, and zip(*pairs) transposes a list of pairs.
Unpacking a Dictionary With **
The ** operator at a call site spreads a dict into keyword arguments. Each key becomes a parameter name; each value becomes the argument:
def greet(greeting, name):
return f"{greeting}, {name}"
params = {"greeting": "Hello", "name": "Alice"}
print(greet(**params))
# Hello, Alice
This code unpacks a dict into keyword arguments. The dict key greeting binds to the greeting parameter; the dict key name binds to the name parameter. The result is the same as calling greet with explicit keyword arguments.
The dict's keys must be strings that match parameter names:
def greet(greeting, name):
return f"{greeting}, {name}"
params = {"greeting": "Hello", "unknown_key": "Alice"}
print(greet(**params))
# TypeError: greet() got an unexpected keyword argument 'unknown_key'
This code attempts to unpack a dict with a key that does not match any parameter. Python raises a type error. Unpacking has the same key-matching rules as explicit keyword arguments.
Combining * and **
A single call can use both:
def render(template, data, *, format="html"):
return f"rendered {template} with {data} as {format}"
args = ["welcome.html", {"name": "Alice"}]
opts = {"format": "pdf"}
print(render(*args, **opts))
# rendered welcome.html with {'name': 'Alice'} as pdf
This code combines list-unpacking for positional arguments with dict-unpacking for keyword arguments. The positional args come from a list; the keyword args from a dict. The call result is the same as supplying each argument explicitly.
The order at the call site mirrors the definition order: positional unpacking first, then keyword unpacking.
Unpacking in Function Forwarding
The most common practical use is forwarding all arguments from a wrapper to a target:
def log_call(func, *args, **kwargs):
print(f"[CALL] {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"[RETURN] {result}")
return result
def divide(a, b):
return a / b
log_call(divide, 10, b=2)
# [CALL] divide((10,), {'b': 2})
# [RETURN] 5.0
This code shows the forwarding pattern. The wrapper accepts star args and double star kwargs, then unpacks them with star and double star when calling the target. The wrapper is agnostic to the target's signature.
This is the foundation of decorators. A decorator wraps a function whose signature it does not control, so it must accept and forward whatever arrives.
Multiple Unpackings in One Call (PEP 448)
Python 3.5 (PEP 448) allowed multiple unpackings in a single call. This is uncommon but legal:
def report(*all_items):
for item in all_items:
print(item)
prefix = [1, 2, 3]
middle = [4, 5, 6]
suffix = [7, 8, 9]
report(*prefix, *middle, *suffix, 99)
# 1 2 3 4 5 6 7 8 9 99
This code unpacks multiple sequences into a single function call. PEP 448 allows multiple star expressions in a single call. The elements are concatenated in the order they appear.
The same applies to **:
def show(**kwargs):
for key, value in kwargs.items():
print(f"{key}={value}")
defaults = {"host": "localhost", "port": 8080}
overrides = {"port": 443, "secure": True}
show(**defaults, **overrides)
# host=localhost
# port=443
# secure=True
This code unpacks two dicts in a single call. Later keys override earlier ones, matching dict-merge semantics. The result has the union of keys, with later values winning for shared keys.
The later unpacking wins for shared keys, matching dict.update semantics. This makes it easy to "override defaults":
def configure(**kwargs):
return kwargs
DEFAULTS = {"timeout": 30, "retries": 3}
print(configure(**DEFAULTS, retries=5))
# {'timeout': 30, 'retries': 5}
This code applies defaults and overrides them at the call site. The double-star unpacks defaults first; the explicit retries argument overrides. Later values always win in keyword binding.
Unpacking in Collection Literals (Same Operators)
The same * and ** operators also work in collection literals - not function calls, but they share syntax:
# Building a list from multiple sources
combined = [*range(3), *"abc", 99]
print(combined)
# [0, 1, 2, 'a', 'b', 'c', 99]
# Building a dict from multiple sources
merged = {**{"a": 1, "b": 2}, **{"c": 3}}
print(merged)
# {'a': 1, 'b': 2, 'c': 3}
This code uses unpacking in literal syntax - building a list with star and a dict with double-star. This is purely literal syntax, not a function call, but the unpacking semantics are the same.
This pattern is common for combining inputs into a single structure. For dicts specifically, Python 3.9 added d1 | d2 as a more readable alternative.
Unpacking Mismatches
The unpacking direction can produce specific errors:
# Unpacking with wrong arity
def f(a, b): ...
f(*[1, 2, 3])
# TypeError: f() takes 2 positional arguments but 3 were given
# Dict unpacking with non-string keys
def f(a, b): ...
f(**{1: 10, 2: 20})
# TypeError: f() got some positional-only arguments passed as keyword arguments
# Dict unpacking with key not matching any parameter
def f(a, b): ...
f(**{"a": 1, "extra": 2})
# TypeError: f() got an unexpected keyword argument 'extra'
# Mixing positional after keyword
def f(a, b): ...
f(**{"a": 1}, 2)
# SyntaxError
This code shows four error patterns from unpacking. Each error has a specific cause. Errors at the call site are caught at runtime - type checkers can catch some but not all in advance.
When to Use Unpacking
Unpacking is the right tool for three patterns:
- Forwarding arguments - wrapping or decorating another function
- Calling with a programmatically built argument list - when args come from a list or dict at runtime
- Merging defaults with overrides -
{**defaults, **overrides}is the canonical pattern
Avoid unpacking when:
- The function takes a fixed, small set of arguments and they are known statically
- The list or dict was constructed specifically for the call - just write the arguments directly
Interactive animation: When unpacking makes a call clearer - config, forwarding, zip(*pairs) - and when it only makes it shorter.
Interview Relevance
Interview: Argument unpacking comes up in:
- Implementing decorators (
func(*args, **kwargs)forwards everything)- Constructing function calls from runtime data
- Merging dicts with
{**a, **b}(Python 3.5+ idiom)- Concatenating sequences in literal form with
*A specific common moment: implementing a generic memoization decorator. The decorator must accept any call signature, so it uses
*args, **kwargs. Storing the cache key requires converting(args, kwargs)to something hashable - a common follow-up question.Another common moment: distinguishing the definition uses of
*and**(collecting) from the call uses (unpacking). Interviewers sometimes ask candidates to explain the duality.Module 30 covers Python interview preparation.
Common Pitfalls
Warning: Unpacking a dict in a call with non-string keys. Keyword names must be strings. Numeric or other keys raise
TypeError.Wrong arity for positional unpacking.
f(*[1, 2, 3])for a two-parameter function raisesTypeError. The same rules as direct calling apply.Unknown keys in
**unpacking. Keys must match parameter names. Misspellings caught at runtime.Multiple unpackings with conflicts.
f(**a, **b)raises ifaandbshare a key withf's parameters. For dict-merge use (not calling), the later wins; for function calls, conflicts raise.Unpacking a string by accident.
f(*"hello")unpacks to five characters, not one string. For a single string argument, writef("hello").Confusing collection literal unpacking with function call unpacking. Both use
*and**, but the contexts differ. Inside[...]or{...}, it builds collections. Insidef(...), it calls a function.Forgetting that unpacking does not change the function's behavior.
f(*list_of_two)is the same asf(list_of_two[0], list_of_two[1]). No magic - just spread.
Try It
Exercise: Practice unpacking:
- Given
point = (3, 4)and a functiondef distance(x, y), calldistancewithpointunpacked.- Given
params = {"host": "localhost", "port": 8080}and a functiondef connect(host, port), callconnectwithparamsunpacked.- Write a
log_and_call(func, *args, **kwargs)wrapper that prints the arguments, calls the function, prints the result, and returns it.- Merge two dicts with
{**defaults, **overrides}where overrides should win for shared keys. Verify the result.- Concatenate three lists into a single new list using
[*a, *b, *c].- Try unpacking a dict with a numeric key into a function call. Observe the error and explain why dict-unpacking requires string keys.