zip and enumerate are two of Python’s most useful built-ins. Once you know them, you’ll spot uses everywhere.
zip — walk two (or more) iterables together
zip(a, b) produces tuples pairing items from each:
names: list[str] = ["alice", "bob", "carol"]
ages: list[int] = [30, 25, 35]
for name, age in zip(names, ages):
print(f"{name} is {age}")
alice is 30
bob is 25
carol is 35
This replaces the un-Pythonic pattern of looping by index:
# avoid
for i in range(len(names)):
print(f"{names[i]} is {ages[i]}")
zip is shorter, faster, and reads naturally.
zip with more than two iterables
You can zip any number of iterables together:
names: list[str] = ["alice", "bob", "carol"]
ages: list[int] = [30, 25, 35]
cities: list[str] = ["chennai", "mumbai", "pune"]
for name, age, city in zip(names, ages, cities):
print(f"{name}, {age}, {city}")
zip stops at the shortest
If the iterables are different lengths, zip stops when the shortest runs out:
list(zip([1, 2, 3, 4], ["a", "b"]))
# [(1, 'a'), (2, 'b')]
If you instead want it to keep going (filling missing values), use itertools.zip_longest:
from itertools import zip_longest
list(zip_longest([1, 2, 3, 4], ["a", "b"], fillvalue="-"))
# [(1, 'a'), (2, 'b'), (3, '-'), (4, '-')]
Building dictionaries with zip
A clean one-liner:
keys: list[str] = ["host", "port", "scheme"]
values: list[str] = ["localhost", "8080", "https"]
config: dict[str, str] = dict(zip(keys, values))
print(config)
# {'host': 'localhost', 'port': '8080', 'scheme': 'https'}
Unzipping with zip
The same zip can also reverse the operation, using the * unpacking we saw in Section 5:
pairs: list[tuple[str, int]] = [("alice", 30), ("bob", 25), ("carol", 35)]
names, ages = zip(*pairs)
print(names) # ('alice', 'bob', 'carol')
print(ages) # (30, 25, 35)
This pattern often shows up when splitting batches of (input, target) pairs.
enumerate — get an index along with each value
When you’re looping over a list and need the position too:
fruits: list[str] = ["apple", "banana", "cherry"]
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")
0: apple
1: banana
2: cherry
Without enumerate, the only options were ugly:
# avoid
for i in range(len(fruits)):
print(f"{i}: {fruits[i]}")
Starting at a different number
By default enumerate starts at 0. Pass a start to change it:
for line_no, line in enumerate(open("file.txt"), start=1):
print(f"{line_no}: {line.rstrip()}")
Useful when you want human-friendly numbering (line 1, line 2, …).
When to use each
zip — multiple iterables, walk them in parallel
enumerate — one iterable, want the position too
If you find yourself writing range(len(x)), the right tool is almost always enumerate or zip.
A real-world example
Pair predictions with their true labels and print the wrong ones:
predictions: list[str] = ["cat", "dog", "cat", "bird", "dog"]
labels: list[str] = ["cat", "cat", "cat", "bird", "fish"]
for i, (pred, label) in enumerate(zip(predictions, labels)):
if pred != label:
print(f"item {i}: predicted {pred}, actual {label}")
item 1: predicted dog, actual cat
item 4: predicted dog, actual fish
enumerate(zip(...)) is one of the most common combinations in ML training scripts.
What’s next
You can pair iterables and number their items. Next, sorted() — sorting in detail.