enumerate, zip, and sorted in Python: Loop Superpowers
Priya has been writing range(len()) for eight months and it works. Kai shows her enumerate, zip, and sorted — and why the readable version is the right one.
You've got a list of orders. Management wants a numbered report — 1 through N, order ID and total on each line. How do you write that loop?
Easy. for i in range(len(orders)): print(i+1, orders[i]['id'], orders[i]['total']). Done. I've written that a hundred times.
I know you have. I've seen the PRs.
Was it that obvious?
Let me show you what Amir would write.
for i, order in enumerate(orders, start=1):
print(i, order['id'], order['total'])
That's shorter. But range(len()) works. What's actually wrong with it?
Nothing is wrong with it. But read it out loud. "For i in range of length of orders" — you're describing the mechanism, not the intent. And there's a real failure mode: orders[i] can throw an IndexError if orders ever isn't a list. enumerate works on any iterable — a file, a generator, a database cursor.
So range(len()) couples me to the fact that it's a list?
Exactly. enumerate says "give me each item with its position." It does not care how the items are stored. And the start=1 argument means you don't need the i+1 trick — the counter starts at 1.
Okay but I've never seen Amir use start= in the codebase.
Because most of the time you want zero-based indexing for internal logic. The start=1 is for when you're generating human-facing output — report line numbers, ranked lists, that sort of thing.
When would I actually need the index though? Most of the time I just want the item.
Right — if you only need the item, use a plain for-loop. enumerate is specifically for when you need both. Numbered reports, finding the position of something, building (index, value) pairs for a log.
OH. That's why Amir's report generator uses enumerate on the order list. I thought that was just style.
It's a signal. When you see enumerate, the reader immediately knows: this code cares about position and value. When you see range(len()), the reader has to work out whether the index is meaningful or just a way to get the item.
Fine. I'll stop fighting it. What's next?
Two lists. Products and prices, stored separately because someone made an early architecture decision that is now everyone's problem.
products = ["Widget", "Gadget", "Doohickey"]
prices = [9.99, 49.99, 4.99]
You need to loop over both at once to build a report. How do you do it?
Same range(len()) move. for i in range(len(products)): print(products[i], prices[i]).
And if products has three items but prices has two?
IndexError.
Silently wrong data if you get the length from the wrong list. zip solves both problems.
for product, price in zip(products, prices):
print(product, price)
It just... pairs them up?
One item from each iterable, in order, until the shorter one runs out. No index arithmetic, no off-by-one risk. You can zip as many iterables as you want.
for product, price, stock in zip(products, prices, stock_counts):
print(product, price, stock)
What if the lists have different lengths? Does it error?
It stops at the shortest. If you want to keep going and fill the gap with a default, use itertools.zip_longest. But for most e-commerce work — pairing SKUs with prices, matching order IDs to totals — the lists should be the same length anyway. If they're not, that's a data integrity bug you want to catch, not paper over.
So zip failing quietly at the shorter list is... intentional? To avoid masking bad data?
That is exactly the right way to think about it. You're asking the right questions now.
I learned from the best. Okay, I've also been writing sorted(orders, key=lambda x: x['total']) in my code and I have no idea if I'm doing it right.
You're doing it right. You just don't know why yet.
sorted takes any iterable and returns a new list. The key= argument is a function that extracts the value to compare. So key=lambda x: x['total'] means "sort by each order's total field."
orders = [
{"id": "A1", "total": 89.50},
{"id": "A2", "total": 12.00},
{"id": "A3", "total": 200.00},
]
by_total = sorted(orders, key=lambda order: order['total'])
# [{"id": "A2", ...}, {"id": "A1", ...}, {"id": "A3", ...}]
And reverse=True flips it?
Exactly. sorted(orders, key=lambda o: o['total'], reverse=True) gives you highest total first — the daily sales summary view.
Wait, I know how to do this with a comprehension now. After Day 3.
Show me.
Like... [order['id'] for order in sorted(orders, key=lambda o: o['total'], reverse=True)]. Get the IDs in total order.
That's exactly how Amir builds the report queue. sorted feeds directly into a comprehension. They compose.
Okay so enumerate gives me index plus item, zip pairs two lists, sorted handles the ranking. These all feel like they should have been in the original for-loop tutorial.
They get left out because basic for-loops work and beginners have enough to absorb. But these three functions are what separate code that works from code that reads. When a teammate opens your report generator, they should not have to decode your indexing arithmetic — they should see the intent in the function names.
Is there anything else in this family?
map, filter, min, max with key= — but you already have comprehensions for most of what map and filter do. The sorted, enumerate, zip trio is the one that shows up in production code every single week.
All of these build a full list in memory though, right? Like sorted returns a list. enumerate makes pairs. If I had a million orders...
That is exactly the right concern, and it is the preview for tomorrow.
Don't tell me. Generators?
Generators. Tomorrow you'll see how to do everything we did today without loading a million orders into memory at once. sorted always needs the full list — it has to see everything to rank it. But the loop patterns? Those can be lazy. One order at a time, as needed.
That's actually what Amir's pipeline does. I never understood why he didn't just sort the whole thing and iterate. There must be a reason.
Tomorrow you'll be able to read that code and explain it to a teammate. That's the goal.
Practice your skills
Sign up to write and run code in this lesson.
enumerate, zip, and sorted in Python: Loop Superpowers
Priya has been writing range(len()) for eight months and it works. Kai shows her enumerate, zip, and sorted — and why the readable version is the right one.
You've got a list of orders. Management wants a numbered report — 1 through N, order ID and total on each line. How do you write that loop?
Easy. for i in range(len(orders)): print(i+1, orders[i]['id'], orders[i]['total']). Done. I've written that a hundred times.
I know you have. I've seen the PRs.
Was it that obvious?
Let me show you what Amir would write.
for i, order in enumerate(orders, start=1):
print(i, order['id'], order['total'])
That's shorter. But range(len()) works. What's actually wrong with it?
Nothing is wrong with it. But read it out loud. "For i in range of length of orders" — you're describing the mechanism, not the intent. And there's a real failure mode: orders[i] can throw an IndexError if orders ever isn't a list. enumerate works on any iterable — a file, a generator, a database cursor.
So range(len()) couples me to the fact that it's a list?
Exactly. enumerate says "give me each item with its position." It does not care how the items are stored. And the start=1 argument means you don't need the i+1 trick — the counter starts at 1.
Okay but I've never seen Amir use start= in the codebase.
Because most of the time you want zero-based indexing for internal logic. The start=1 is for when you're generating human-facing output — report line numbers, ranked lists, that sort of thing.
When would I actually need the index though? Most of the time I just want the item.
Right — if you only need the item, use a plain for-loop. enumerate is specifically for when you need both. Numbered reports, finding the position of something, building (index, value) pairs for a log.
OH. That's why Amir's report generator uses enumerate on the order list. I thought that was just style.
It's a signal. When you see enumerate, the reader immediately knows: this code cares about position and value. When you see range(len()), the reader has to work out whether the index is meaningful or just a way to get the item.
Fine. I'll stop fighting it. What's next?
Two lists. Products and prices, stored separately because someone made an early architecture decision that is now everyone's problem.
products = ["Widget", "Gadget", "Doohickey"]
prices = [9.99, 49.99, 4.99]
You need to loop over both at once to build a report. How do you do it?
Same range(len()) move. for i in range(len(products)): print(products[i], prices[i]).
And if products has three items but prices has two?
IndexError.
Silently wrong data if you get the length from the wrong list. zip solves both problems.
for product, price in zip(products, prices):
print(product, price)
It just... pairs them up?
One item from each iterable, in order, until the shorter one runs out. No index arithmetic, no off-by-one risk. You can zip as many iterables as you want.
for product, price, stock in zip(products, prices, stock_counts):
print(product, price, stock)
What if the lists have different lengths? Does it error?
It stops at the shortest. If you want to keep going and fill the gap with a default, use itertools.zip_longest. But for most e-commerce work — pairing SKUs with prices, matching order IDs to totals — the lists should be the same length anyway. If they're not, that's a data integrity bug you want to catch, not paper over.
So zip failing quietly at the shorter list is... intentional? To avoid masking bad data?
That is exactly the right way to think about it. You're asking the right questions now.
I learned from the best. Okay, I've also been writing sorted(orders, key=lambda x: x['total']) in my code and I have no idea if I'm doing it right.
You're doing it right. You just don't know why yet.
sorted takes any iterable and returns a new list. The key= argument is a function that extracts the value to compare. So key=lambda x: x['total'] means "sort by each order's total field."
orders = [
{"id": "A1", "total": 89.50},
{"id": "A2", "total": 12.00},
{"id": "A3", "total": 200.00},
]
by_total = sorted(orders, key=lambda order: order['total'])
# [{"id": "A2", ...}, {"id": "A1", ...}, {"id": "A3", ...}]
And reverse=True flips it?
Exactly. sorted(orders, key=lambda o: o['total'], reverse=True) gives you highest total first — the daily sales summary view.
Wait, I know how to do this with a comprehension now. After Day 3.
Show me.
Like... [order['id'] for order in sorted(orders, key=lambda o: o['total'], reverse=True)]. Get the IDs in total order.
That's exactly how Amir builds the report queue. sorted feeds directly into a comprehension. They compose.
Okay so enumerate gives me index plus item, zip pairs two lists, sorted handles the ranking. These all feel like they should have been in the original for-loop tutorial.
They get left out because basic for-loops work and beginners have enough to absorb. But these three functions are what separate code that works from code that reads. When a teammate opens your report generator, they should not have to decode your indexing arithmetic — they should see the intent in the function names.
Is there anything else in this family?
map, filter, min, max with key= — but you already have comprehensions for most of what map and filter do. The sorted, enumerate, zip trio is the one that shows up in production code every single week.
All of these build a full list in memory though, right? Like sorted returns a list. enumerate makes pairs. If I had a million orders...
That is exactly the right concern, and it is the preview for tomorrow.
Don't tell me. Generators?
Generators. Tomorrow you'll see how to do everything we did today without loading a million orders into memory at once. sorted always needs the full list — it has to see everything to rank it. But the loop patterns? Those can be lazy. One order at a time, as needed.
That's actually what Amir's pipeline does. I never understood why he didn't just sort the whole thing and iterate. There must be a reason.
Tomorrow you'll be able to read that code and explain it to a teammate. That's the goal.