A common shape — iterate a list, but you also want to know the index:
names = ["Ada", "Bob", "Cleo"]
# Manual index — works, but ugly
i = 0
for name in names:
print(i, name)
i += 1I've written that pattern. There must be a better way.
enumerate. Wraps any iterable and yields (index, item) pairs:
for i, name in enumerate(names):
print(i, name)
# 0 Ada
# 1 Bob
# 2 CleoThe second argument to enumerate is the starting index — useful when humans want 1-based numbering:
for i, name in enumerate(names, 1):
print(f"{i}. {name}")
# 1. Ada
# 2. Bob
# 3. CleoAnd iterating two lists at once?
zip. Takes two (or more) iterables and yields tuples of corresponding items:
names = ["Ada", "Bob", "Cleo"]
ages = [36, 41, 28]
for name, age in zip(names, ages):
print(name, age)
# Ada 36
# Bob 41
# Cleo 28What if the two lists are different lengths?
zip stops at the shortest. The longer iterable's tail is silently dropped. If you want to fail loudly, use zip(names, ages, strict=True) (Python 3.10+) — that raises ValueError on mismatch.
Anti-patterns to avoid?
Two:
for i in range(len(xs)). Use enumerate instead — same effect, fewer pieces.for i in range(len(xs)): print(xs[i], ys[i]). Use zip instead — clearer intent.If you find yourself writing range(len(...)), ask whether enumerate or zip does the same thing more directly.
enumerate(iterable, start=0) — index + item pairsfor i, item in enumerate(["a", "b", "c"]):
print(i, item)
# 0 a
# 1 b
# 2 cstart parameterfor i, item in enumerate(["a", "b", "c"], start=1):
print(i, item)
# 1 a
# 2 b
# 3 cUse start=1 for human-facing line numbering.
# Don't write this
for i in range(len(xs)):
process(i, xs[i])
# Write this
for i, x in enumerate(xs):
process(i, x)zip(*iterables) — pair items in parallelfor n, a in zip(["Ada", "Bob"], [36, 41]):
print(n, a)
# Ada 36
# Bob 41for a, b, c in zip(xs, ys, zs):
...No limit on how many iterables you pass.
list(zip([1, 2, 3], ["a", "b"])) # [(1, 'a'), (2, 'b')]The trailing 3 is dropped silently.
strict=True (Python 3.10+)list(zip([1, 2, 3], ["a", "b"], strict=True))
# ValueError: zip() argument 2 is shorter than argument 1Use when matching lengths is a precondition you want to enforce.
keys = ["a", "b", "c"]
values = [1, 2, 3]
dict(zip(keys, values)) # {'a': 1, 'b': 2, 'c': 3}enumerate and zipfor i, (name, age) in enumerate(zip(names, ages), 1):
print(f"{i}. {name} ({age})")The inner zip produces (name, age) pairs; enumerate adds the index. Note the parens around (name, age) — you're unpacking one item that happens to be a tuple.
| Bad | Good |
|---|---|
for i in range(len(xs)): ... xs[i] | for i, x in enumerate(xs) |
for i in range(len(xs)): ... xs[i], ys[i] | for x, y in zip(xs, ys) |
Manual i = 0; for ...; i += 1 | for i, x in enumerate(xs) |
Any time you reach for an index only because you need to look up another list — zip is your tool.
A common shape — iterate a list, but you also want to know the index:
names = ["Ada", "Bob", "Cleo"]
# Manual index — works, but ugly
i = 0
for name in names:
print(i, name)
i += 1I've written that pattern. There must be a better way.
enumerate. Wraps any iterable and yields (index, item) pairs:
for i, name in enumerate(names):
print(i, name)
# 0 Ada
# 1 Bob
# 2 CleoThe second argument to enumerate is the starting index — useful when humans want 1-based numbering:
for i, name in enumerate(names, 1):
print(f"{i}. {name}")
# 1. Ada
# 2. Bob
# 3. CleoAnd iterating two lists at once?
zip. Takes two (or more) iterables and yields tuples of corresponding items:
names = ["Ada", "Bob", "Cleo"]
ages = [36, 41, 28]
for name, age in zip(names, ages):
print(name, age)
# Ada 36
# Bob 41
# Cleo 28What if the two lists are different lengths?
zip stops at the shortest. The longer iterable's tail is silently dropped. If you want to fail loudly, use zip(names, ages, strict=True) (Python 3.10+) — that raises ValueError on mismatch.
Anti-patterns to avoid?
Two:
for i in range(len(xs)). Use enumerate instead — same effect, fewer pieces.for i in range(len(xs)): print(xs[i], ys[i]). Use zip instead — clearer intent.If you find yourself writing range(len(...)), ask whether enumerate or zip does the same thing more directly.
enumerate(iterable, start=0) — index + item pairsfor i, item in enumerate(["a", "b", "c"]):
print(i, item)
# 0 a
# 1 b
# 2 cstart parameterfor i, item in enumerate(["a", "b", "c"], start=1):
print(i, item)
# 1 a
# 2 b
# 3 cUse start=1 for human-facing line numbering.
# Don't write this
for i in range(len(xs)):
process(i, xs[i])
# Write this
for i, x in enumerate(xs):
process(i, x)zip(*iterables) — pair items in parallelfor n, a in zip(["Ada", "Bob"], [36, 41]):
print(n, a)
# Ada 36
# Bob 41for a, b, c in zip(xs, ys, zs):
...No limit on how many iterables you pass.
list(zip([1, 2, 3], ["a", "b"])) # [(1, 'a'), (2, 'b')]The trailing 3 is dropped silently.
strict=True (Python 3.10+)list(zip([1, 2, 3], ["a", "b"], strict=True))
# ValueError: zip() argument 2 is shorter than argument 1Use when matching lengths is a precondition you want to enforce.
keys = ["a", "b", "c"]
values = [1, 2, 3]
dict(zip(keys, values)) # {'a': 1, 'b': 2, 'c': 3}enumerate and zipfor i, (name, age) in enumerate(zip(names, ages), 1):
print(f"{i}. {name} ({age})")The inner zip produces (name, age) pairs; enumerate adds the index. Note the parens around (name, age) — you're unpacking one item that happens to be a tuple.
| Bad | Good |
|---|---|
for i in range(len(xs)): ... xs[i] | for i, x in enumerate(xs) |
for i in range(len(xs)): ... xs[i], ys[i] | for x, y in zip(xs, ys) |
Manual i = 0; for ...; i += 1 | for i, x in enumerate(xs) |
Any time you reach for an index only because you need to look up another list — zip is your tool.
Create a free account to get started. Paid plans unlock all tracks.