enumerate(iterable) pairs each item with a running index, yielding
(index, value) tuples. It saves you from the error-prone manual counter
or range(len(...)) pattern. The optional start argument sets the
first index (default 0).
colors = ["red", "green", "blue"]
for i, c in enumerate(colors):
print(i, c) # 0 red / 1 green / 2 blue
for rank, c in enumerate(colors, start=1):
print(rank, c) # 1 red / 2 green / 3 blue
Prefer enumerate over range(len(seq)) whenever you need both the index
and the item — it's more readable and works on any iterable, not just
indexable sequences.
zip(a, b, ...) walks several iterables in parallel, yielding tuples of
aligned elements. It stops at the shortest input, silently dropping
extras. itertools.zip_longest instead runs to the longest, filling
missing slots with a fillvalue.
names = ["ada", "grace", "edsger"]
ages = [36, 45]
list(zip(names, ages)) # [('ada', 36), ('grace', 45)] — 'edsger' dropped
from itertools import zip_longest
list(zip_longest(names, ages, fillvalue=0))
# [('ada', 36), ('grace', 45), ('edsger', 0)]
Use plain zip when lengths match (or truncation is intended); reach for
zip_longest when you must not lose data from the longer iterable.
zip is its own inverse. Applying zip(*pairs) unpacks the list of
tuples as separate arguments, so zip re-groups them column-wise —
effectively transposing rows into columns.
pairs = [("ada", 36), ("grace", 45)]
names, ages = zip(*pairs)
names # ('ada', 'grace')
ages # (36, 45)
This also transposes a matrix: list(zip(*matrix)). Note the results are
tuples, not lists, and the input is consumed if it's an iterator — wrap
in list(...) if you need list results.
A starred target in an assignment captures "the rest" as a list. You can
place *name at the start, middle, or end, and the non-starred names take
fixed positions — Python figures out the split.
first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4]
*init, last = [1, 2, 3, 4] # init=[1, 2, 3], last=4
head, *mid, tail = [1, 2, 3, 4] # head=1, mid=[2, 3], tail=4
Exactly one starred target is allowed per assignment, and it always
becomes a list (even if empty). This is cleaner than slicing for grabbing
the head/tail of a sequence.
Pairing two sequences with zip and feeding them to dict() is the
idiomatic way to build a mapping. The * and ** operators also unpack
iterables/dicts into function calls as positional and keyword
arguments.
keys = ["x", "y", "z"]
vals = [1, 2, 3]
d = dict(zip(keys, vals)) # {'x': 1, 'y': 2, 'z': 3}
def point(x, y, z): return (x, y, z)
args = [1, 2, 3]
point(*args) # unpack list -> positional
point(**d) # unpack dict -> keyword args
Use * to spread a sequence into positional parameters and ** to spread a
dict into keyword parameters — the call-site mirror of *args/**kwargs in
a function signature.
In Python 3.10+, zip(a, b, strict=True) raises ValueError if the iterables
have different lengths, instead of silently truncating. It catches a class of
bugs where you assumed two sequences were aligned.
list(zip([1, 2, 3], ["a", "b"])) # [(1,'a'),(2,'b')] — silent loss
list(zip([1, 2, 3], ["a", "b"], strict=True)) # ValueError — length mismatch
Rule of thumb: pass strict=True when equal lengths are an invariant — fail loudly
rather than silently dropping data from the longer iterable.
enumerate gives you the index and the item directly, works on any iterable
(not just indexable sequences), and avoids repeated seq[i] lookups. range(len())
is verbose, error-prone, and breaks on generators.
# clunky and index-only:
for i in range(len(colors)):
print(i, colors[i])
# clear, item in hand, works on any iterable:
for i, c in enumerate(colors):
print(i, c)
Rule of thumb: if you find yourself writing range(len(x)), you almost always want
enumerate(x) instead.
zip shines inside comprehensions to combine aligned sequences element-wise —
summing pairs, building records, or comparing columns — in one readable expression.
prices = [10, 20, 30]
qtys = [2, 1, 5]
totals = [p * q for p, q in zip(prices, qtys)] # [20, 20, 150]
records = [{"price": p, "qty": q} for p, q in zip(prices, qtys)]
grand = sum(p * q for p, q in zip(prices, qtys)) # 190
Rule of thumb: for a, b in zip(xs, ys) inside a comprehension is the idiom for
element-wise combination of parallel sequences.
You can destructure nested structure directly in the loop target, mirroring the
shape of each item. This is common with enumerate over pairs or zipped tuples.
pairs = [(1, ("a", "b")), (2, ("c", "d"))]
for num, (left, right) in pairs:
print(num, left, right) # 1 a b / 2 c d
for i, (name, age) in enumerate(zip(names, ages)):
... # index + destructured pair
Rule of thumb: match the loop target's shape to the item's shape — nested parentheses unpack nested tuples without manual indexing.
Yes — you can interleave unpacked iterables with explicit positional args, and even
use multiple * unpackings in one call (3.5+). The same applies to ** for
keywords. They're expanded left to right.
def f(a, b, c, d): return (a, b, c, d)
pair = [2, 3]
f(1, *pair, 4) # (1, 2, 3, 4) — star in the middle
f(*[1, 2], *[3, 4]) # (1, 2, 3, 4) — multiple unpackings
d1 = {"a": 1}; d2 = {"b": 2}
dict(**d1, **d2) # {'a': 1, 'b': 2} — merge via **
Rule of thumb: */** can be combined with literals and each other in a call;
Python flattens them positionally/by-keyword in order.
Since 3.5, * unpacks iterables into list/tuple/set literals and ** unpacks dicts
into dict literals — a clean way to merge or extend without +, update, or
extend.
a = [1, 2]; b = [3, 4]
[*a, *b, 5] # [1, 2, 3, 4, 5]
{*a, *b} # {1, 2, 3, 4}
d1 = {"x": 1}; d2 = {"y": 2}
{**d1, **d2, "z": 3} # {'x': 1, 'y': 2, 'z': 3} — later keys win
Rule of thumb: [*a, *b] and {**d1, **d2} are the idiomatic literal-merge forms;
with dicts, rightmost duplicate keys override earlier ones.
Yes — in Python 3 zip returns a lazy iterator, not a list. It produces tuples
on demand and is single-pass: once consumed, it's exhausted. Wrap in list() to
materialize or reuse.
z = zip([1, 2], ["a", "b"])
list(z) # [(1, 'a'), (2, 'b')]
list(z) # [] — already exhausted!
pairs = list(zip(xs, ys)) # materialize if you need it twice
Rule of thumb: zip is a one-shot iterator — convert to a list if you must iterate
more than once or index into it.
Yes — enumerate accepts any iterable, including generators, files, and
zip/map objects, because it only pulls items one at a time. This is a key
advantage over range(len()), which needs a sized, indexable sequence.
with open("data.txt") as f:
for lineno, line in enumerate(f, start=1): # works on a file iterator
print(lineno, line.rstrip())
for i, sq in enumerate(n * n for n in range(5)): # works on a generator
...
Rule of thumb: enumerate is universal across iterables; len-based indexing is not
— prefer enumerate for streaming sources.
a, b = b, a works because the right side is evaluated into a tuple first, then
unpacked into the targets. No temporary variable is needed, and it generalizes to
any number of names.
a, b = 1, 2
a, b = b, a # a=2, b=1
x, y, z = 1, 2, 3
x, y, z = z, x, y # rotate: x=3, y=1, z=2
# common in algorithms (e.g. Fibonacci):
a, b = b, a + b
Rule of thumb: the right-hand side is fully evaluated before assignment, so multi-target swaps/rotations are safe and atomic-looking.
Plain zip silently truncates to the shortest iterable, so mismatched lengths
lose data without any warning. If one source is an iterator, zip also consumes
one extra element from the longer one while detecting the stop.
a = iter([1, 2, 3])
b = [10, 20]
list(zip(a, b)) # [(1, 10), (2, 20)]
list(a) # [] or [3]? — 3 may already be consumed by zip
Rule of thumb: when lengths should match, use strict=True; when they shouldn't,
use zip_longest — don't rely on silent truncation, and beware consuming iterators.
More Comprehensions & Iteration interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.