enumerate, zip, and unpacking, explained
These tools replace clunky index-juggling with clear, Pythonic loops. Reaching for
range(len(...)) in an interview is a small tell; enumerate and zip are what
experienced Python developers use. This guide covers both, plus the star-unpacking tricks
that go with them.
enumerate — index and value together
When you need the index and the item, use enumerate instead of range(len(...)):
for i, name in enumerate(["a", "b", "c"]):
print(i, name) # 0 a / 1 b / 2 c
# the un-Pythonic version it replaces:
for i in range(len(names)):
print(i, names[i])
Set a different starting index with start:
for rank, name in enumerate(["a", "b"], start=1):
print(rank, name) # 1 a / 2 b
zip — iterate multiple sequences in lockstep
zip pairs up items from several iterables, yielding tuples until the shortest runs
out:
names = ["Ada", "Alan"]
ages = [36, 41]
for name, age in zip(names, ages):
print(name, age) # Ada 36 / Alan 41
Because it stops at the shortest, extra items in a longer iterable are silently dropped:
list(zip([1, 2, 3], ["a", "b"])) # [(1, 'a'), (2, 'b')] — the 3 is lost
zip_longest when lengths differ
If you'd rather pad to the longest, use itertools.zip_longest:
from itertools import zip_longest
list(zip_longest([1, 2, 3], ["a"], fillvalue="?"))
# [(1, 'a'), (2, '?'), (3, '?')]
Unzipping with zip(*)
The same zip "transposes" back when you unpack a sequence of pairs with *:
pairs = [(1, "a"), (2, "b"), (3, "c")]
nums, letters = zip(*pairs)
nums # (1, 2, 3)
letters # ('a', 'b', 'c')
zip(*pairs) feeds each pair as a separate argument, so zip lines up all the firsts, then
all the seconds.
Building a dict from two sequences
zip plus dict is the idiomatic way to combine keys and values:
keys = ["x", "y", "z"]
vals = [1, 2, 3]
dict(zip(keys, vals)) # {'x': 1, 'y': 2, 'z': 3}
Extended (star) unpacking
The * operator captures "the rest" into a list during unpacking — useful for splitting
off the head or tail:
first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4]
*init, last = [1, 2, 3, 4] # init=[1, 2, 3], last=4
a, *mid, z = [1, 2, 3, 4, 5] # a=1, mid=[2, 3, 4], z=5
Only one starred name is allowed per unpacking, and it always collects into a list.
Star args in a call
The flip side: * in a call spreads an iterable into positional arguments, and **
spreads a dict into keyword arguments:
def point(x, y, z):
return (x, y, z)
coords = [1, 2, 3]
point(*coords) # same as point(1, 2, 3)
kwargs = {"x": 1, "y": 2, "z": 3}
point(**kwargs) # same as point(x=1, y=2, z=3)
Recap
Use enumerate (with an optional start) instead of range(len(...)) when you need
indexes, and zip to walk several iterables in lockstep — it stops at the shortest, so
use itertools.zip_longest to pad. zip(*pairs) unzips, and dict(zip(keys, vals)) builds
a mapping. Star unpacking (first, *rest = ...) splits a sequence into head and tail,
while * and ** in a call spread an iterable or dict into arguments.