Python: enumerate() and zip()
There are two functions that, once you get familiar with them, make some loops in Python look noticeably cleaner — enumerate() and zip().
Both are quite simple, so it's better to jump straight into examples.
enumerate()
Suppose we have a list of languages:
languages = ["Python", "Go", "Rust"]
We want to print not only the name, but also its number.
We could do it like this:
for i in range(len(languages)):
print(i, languages[i])
It works, but it looks a bit like a workaround. We get the length of the list, build a range(), and then use the index to access the list again.
Python has enumerate() for this:
for i, language in enumerate(languages):
print(i, language)
Result:
0 Python
1 Go
2 Rust
Essentially, enumerate() adds a counter to each element as you iterate over it.
list(enumerate(languages))
[
(0, "Python"),
(1, "Go"),
(2, "Rust"),
]
And this:
for i, language in enumerate(languages):
simply unpacks these pairs into two variables.
Often, numbering from zero isn't useful at all. For example, if we're displaying a ranking:
languages = ["Python", "Go", "Rust"]
for position, language in enumerate(languages, start=1):
print(f"{position}. {language}")
We'll get:
1. Python
2. Go
3. Rust
I think this is one of those cases where the code reads almost like ordinary text.
By the way, the number produced by enumerate() is more accurately called a counter rather than an index. With a regular enumerate(languages), it matches the list index, but nothing stops us from writing:
enumerate(languages, start=42)
The list itself, of course, won't suddenly start being indexed from 42.
zip()
Now let's look at another common situation.
We have some names:
names = ["Alice", "Bob", "Charlie"]
And ages stored separately:
ages = [25, 31, 28]
We need to iterate over them at the same time.
Again, we could use indices:
for i in range(len(names)):
print(names[i], ages[i])
But this is simpler:
for name, age in zip(names, ages):
print(name, age)
We'll get:
Alice 25
Bob 31
Charlie 28
The name zip() is quite fitting here. The function sort of zips two sequences together.
Alice 25
Bob 31
Charlie 28
If we look at the result using list():
list(zip(names, ages))
we'll get:
[
("Alice", 25),
("Bob", 31),
("Charlie", 28),
]
And there can be any number of sequences:
names = ["Alice", "Bob", "Charlie"]
ages = [25, 31, 28]
cities = ["London", "Berlin", "Madrid"]
for name, age, city in zip(names, ages, cities):
print(name, age, city)
There's no need to work with indices here anymore.
There's one nuance with zip()
What happens if the lists have different lengths?
names = ["Alice", "Bob", "Charlie"]
ages = [25, 31]
for name, age in zip(names, ages):
print(name, age)
Result:
Alice 25
Bob 31
Charlie simply won't make it into the loop.
zip() stops when the shortest sequence is exhausted.
Sometimes that's exactly what you want. And sometimes it's a bug you'd rather notice.
If the lengths must match, you can write:
for name, age in zip(names, ages, strict=True):
print(name, age)
Then, if the lengths don't match, we'll get a ValueError.
zip() is handy for creating dictionaries
We have a list of fields:
fields = ["name", "age", "city"]
And a list of values:
values = ["Alice", 25, "London"]
We can combine them:
user = dict(zip(fields, values))
And get:
{
"name": "Alice",
"age": 25,
"city": "London",
}
It's a simple technique, but it comes in handy from time to time.
And you can use them together
Suppose we have participants and their scores:
names = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 91]
We want to print:
1. Alice — 95
2. Bob — 87
3. Charlie — 91
Then we can combine both functions:
for position, (name, score) in enumerate(
zip(names, scores),
start=1,
):
print(f"{position}. {name} — {score}")
At first glance, this construction may look a little strange:
position, (name, score)
But if we look at the data, it becomes clearer.
zip() gives us:
("Alice", 95)
("Bob", 87)
("Charlie", 91)
And enumerate() adds a number to each such pair:
(1, ("Alice", 95))
(2, ("Bob", 87))
(3, ("Charlie", 91))
That's where this unpacking comes from:
position, (name, score)
The main idea here is the same as with many other Python features: if you find yourself manually managing indices, it's worth first checking whether there's a way to express the task directly.