All 57 lessons

Lesson 11 of 57 · Control Flow

Loop Patterns: enumerate, zip, Accumulators and Nested Loops

By Skillancy Editorial Team

Run the code in the free Python Compiler or on your own computer. Lessons that read files use the FreshBasket practice data: download the dataset (ZIP).

By now you know how for and while work. The next step is learning the patterns that experienced programmers reach for again and again. A pattern is a small, reusable shape for solving a common problem: add everything up, count the matches, find the biggest, compare each item with the previous one, pair two lists, number the items, build a filtered copy.

Learning these shapes has two payoffs. They make your Python code short and readable, and they translate directly to pandas, where the same ideas appear as sum, count, max, diff, zip-like column pairing and boolean filtering. This lesson closes Module 2 by assembling everything about control flow into a toolkit, using FreshBasket data.

The pattern toolkit at a glance

GoalPatternPandas cousin
Totaltotal = 0 then total += x.sum()
Count matchescount += 1 inside an if(cond).sum()
Biggest and wherekeep best and best_pos.max(), .idxmax()
Running totalappend total each pass.cumsum()
Change vs previousloop from index 1.diff(), .pct_change()
Filter[x for x in items if cond]df[cond]
Transform[f(x) for x in items].apply(), vectorised maths
Pair two listszip(a, b)columns of a DataFrame

Worked examples

We use a small set of real monthly figures for this lesson: orders and revenue (in ₹ thousands) for FreshBasket Pune, first half of 2026.

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
orders = [310, 295, 342, 388, 415, 460]
revenue_k = [158, 149, 176, 201, 224, 249]

Example 1: enumerate gives position and value

Question: Print the months numbered from 1, with their orders.

for number, (month, n) in enumerate(zip(months, orders), start=1):
    print(f"{number}. {month}: {n} orders")

Output

1. Jan: 310 orders
2. Feb: 295 orders
3. Mar: 342 orders
4. Apr: 388 orders
5. May: 415 orders
6. Jun: 460 orders

How it works: enumerate(iterable, start=1) produces pairs of (counter, item). Here the item is itself a pair from zip, so we unpack the nested structure number, (month, n) in the for line. Without start, counting begins at 0, which matches list positions.

Reading the result: Six numbered lines, January to June. Use enumerate instead of range(len(items)) whenever you need both the position and the value; it removes a whole class of indexing mistakes.

Example 2: zip pairs lists and computes per-item results

Question: Compute the average basket value (₹) for each month from the two lists.

for month, n, rev in zip(months, orders, revenue_k):
    avg_basket = rev * 1000 / n
    print(f"{month}: Rs. {avg_basket:,.0f}")

Output

Jan: Rs. 510
Feb: Rs. 505
Mar: Rs. 515
Apr: Rs. 518
May: Rs. 540
Jun: Rs. 541

How it works: zip walks the three lists in step, handing us one element from each per pass. Revenue is in thousands, so we multiply by 1000 before dividing by the number of orders.

Reading the result: Average baskets stay near ₹510 in January and ₹540 in June. If lists have different lengths, zip silently stops at the shortest, so check lengths first when data comes from different sources.

Example 3: Running (cumulative) total

Question: Build the year-to-date orders after each month.

running = []
total = 0

for n in orders:
    total += n
    running.append(total)

print(running)

Output

[310, 605, 947, 1335, 1750, 2210]

How it works: The accumulator total grows each pass, and we append a snapshot of it to a new list. A snapshot is needed because total keeps changing; the list keeps each intermediate value.

Reading the result: By June the cumulative total is 2210, which equals sum(orders) (2210). Pandas does this in one call with .cumsum(), but the loop shows what that method does.

Example 4: Comparing each value with the previous one

Question: What was the month-on-month growth in orders, in percent?

growth = []
for i in range(1, len(orders)):
    change = (orders[i] - orders[i - 1]) / orders[i - 1] * 100
    growth.append(round(change, 1))

print(growth)

Output

[-4.8, 15.9, 13.5, 7.0, 10.8]

How it works: We start the loop at index 1 because the first month has no previous month. For each position we use orders[i] and its neighbour orders[i - 1]. The formula is the same percentage change you met in the numbers lesson.

Reading the result: The growth list has 5 entries for 6 months. February dipped 4.8%, and the strongest month was Mar at 15.9%. The + 1 is needed because growth[0] describes the second month.

Example 5: Minimum and maximum with position

Question: Which month had the highest orders, and which had the lowest?

best_i = worst_i = 0

for i in range(len(orders)):
    if orders[i] > orders[best_i]:
        best_i = i
    if orders[i] < orders[worst_i]:
        worst_i = i

print(months[best_i], orders[best_i])
print(months[worst_i], orders[worst_i])

# The built-in shortcut gives the same answer
print(max(orders), min(orders), orders.index(max(orders)))

Output

Jun 460
Feb 295
460 295 5

How it works: We track the position of the best and worst values, not just the values, so we can look up the month name. Python's max, min and the list method index do the same work in one line each, and you should use them in practice.

Reading the result: The peak was Jun with 460 orders, and the low was Feb with 295. The loop and the built-ins agree.

Example 6: Filtering into a new list

Question: Collect the months where orders exceeded 350, and the revenue for those months.

strong_months = []
strong_revenue = 0

for month, n, rev in zip(months, orders, revenue_k):
    if n > 350:
        strong_months.append(month)
        strong_revenue += rev

print(strong_months, strong_revenue)

Output

['Apr', 'May', 'Jun'] 674

How it works: This is the filter pattern: an if inside the loop decides whether to keep an item. We built two results at once, a list of names and a running sum of revenue.

Reading the result: 3 months passed the bar (Apr, May, Jun), together bringing ₹674k. In the comprehension lesson you will write this filter in a single line, and in pandas it becomes df[df["orders"] > 350].

Example 7: Transforming every item

Question: Convert revenue from thousands to lakhs (1 lakh = 100 thousand) and format as text.

revenue_lakh = []
for r in revenue_k:
    revenue_lakh.append(round(r / 100, 2))

labels = []
for m, r in zip(months, revenue_lakh):
    labels.append(f"{m}: {r} L")

print(labels)

Output

['Jan: 1.58 L', 'Feb: 1.49 L', 'Mar: 1.76 L', 'Apr: 2.01 L', 'May: 2.24 L', 'Jun: 2.49 L']

How it works: The transform pattern creates an empty list, loops, and appends a changed version of each item. Two chained loops produce a list of numbers and then a list of labels. Starting with an empty list and calling append is the single most common loop pattern in Python.

Reading the result: January's ₹158k becomes 1.58 lakh, and June's becomes 2.49 lakh. We kept the original lists unchanged, which is the safe way to transform data.

Example 8: reversed and sorted

Question: Show the months from latest to earliest, and rank months by orders without changing the original lists.

for m in reversed(months):
    print(m, end=" ")
print()

ranked = sorted(zip(orders, months), reverse=True)
print(ranked)
print(months)

Output

Jun May Apr Mar Feb Jan 
[(460, 'Jun'), (415, 'May'), (388, 'Apr'), (342, 'Mar'), (310, 'Jan'), (295, 'Feb')]
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']

How it works: reversed() walks backwards. sorted() returns a new sorted list, leaving the original alone (unlike the list method .sort(), which changes the list in place). Sorting a list of pairs orders by the first element, so we put orders first, then flip the order with reverse=True to get the highest first.

Reading the result: The months print June to January, and the ranking begins with (460, 'Jun'). The original months list is untouched, as the last line shows.

Example 9: Nested loops for a grid

Question: Create a small matrix showing orders if each store delivered 1x, 2x or 3x of its base volume.

base = [120, 80, 45]
factors = [1, 2, 3]

print("Factor " + " ".join(f"S{i + 1:>5}" for i in range(len(base))))
for f in factors:
    row = []
    for b in base:
        row.append(b * f)
    print(f"{f:>6} " + " ".join(f"{v:>6}" for v in row))

Output

Factor S    1 S    2 S    3
     1    120     80     45
     2    240    160     90
     3    360    240    135

How it works: The outer loop picks a factor, the inner loop multiplies each store's base by it, and we print one formatted row per factor. The " ".join(...) with a generator inside builds the aligned text row.

Reading the result: The grid has 3 rows and 3 columns, and every cell is base × factor. This shape (a loop within a loop producing rows and columns) is the manual version of a pandas pivot table.

Example 10: Counting with a dictionary (a preview)

Question: Count how many orders used each payment method in this short list.

payments = ["UPI", "Card", "UPI", "COD", "UPI", "Wallet", "Card", "UPI"]

counts = {}
for p in payments:
    if p in counts:
        counts[p] += 1
    else:
        counts[p] = 1

print(counts)

Output

{'UPI': 4, 'Card': 2, 'COD': 1, 'Wallet': 1}

How it works: A dictionary stores key-value pairs. For each payment method we either increase its counter or create it with a count of 1. The next module covers dictionaries properly, including shortcuts such as collections.Counter and dict.get.

Reading the result: UPI appears 4 times and is the most popular method. Counting categories is so common that pandas gives it a one-liner, value_counts().

Common mistakes

1. Changing a list while looping over it. Building a new list is safer.

2. Mixing up index and value. for i in orders gives values, not positions. Use enumerate or range(len(...)) when you need positions.

3. Starting a neighbour comparison at 0.

data = [10, 20, 30]
print(data[0 - 1])

Output

30

Index -1 is the last element, so data[i - 1] at i = 0 silently reads the wrong value instead of raising an error. Start at 1.

4. Using zip on lists of unequal length without noticing. The longer list's extras are dropped.

5. Writing a loop where a built-in exists. sum, max, min, any, all, sorted and len are faster and clearer than manual loops.

Practice questions

Practice 1 (Easy): Numbered cities

Print the list ["Chennai", "Pune", "Kolkata"] as 1) Chennai, 2) Pune, 3) Kolkata.

Show solution
for i, city in enumerate(["Chennai", "Pune", "Kolkata"], start=1):
    print(f"{i}) {city}")

Output

1) Chennai
2) Pune
3) Kolkata

enumerate with start=1 supplies the counter, so there is no manual bookkeeping.

Practice 2 (Medium): Revenue per order and best month

Using months, orders and revenue_k, find the month with the highest revenue per order and print it with the value in rupees, rounded.

Show solution
best_month, best_value = "", 0

for m, n, rev in zip(months, orders, revenue_k):
    value = rev * 1000 / n
    if value > best_value:
        best_month, best_value = m, value

print(best_month, round(best_value))

Output

Jun 541

We keep the best value seen so far together with its month name, replacing both when a better one appears. The highest basket was in Jun at ₹541.

Practice 3 (Hard): Streak detection

Find the longest streak of consecutive months in which orders grew compared with the previous month, and print its length. Use the orders list.

Show solution
longest = current = 0

for i in range(1, len(orders)):
    if orders[i] > orders[i - 1]:
        current += 1
        longest = max(longest, current)
    else:
        current = 0

print(longest)

Output

4

current counts the growth streak that is still running and resets to zero whenever a month fails to grow. longest remembers the best streak. The orders grew from March to June, so the longest streak is 4 months.

Key takeaways

  • enumerate gives position plus value, and zip walks several lists in step; both remove manual index handling.
  • The core accumulator patterns are total, count, best-so-far (with position), and running total, always initialised before the loop.
  • To compare neighbours, start at index 1 and use i and i - 1.
  • Filter and transform by appending to a new list; avoid modifying the list you are looping over.
  • sorted() and reversed() give new orderings without touching the original list.
  • Prefer built-ins (sum, max, min, any, all) when they exist, and remember each pattern has a one-line pandas equivalent coming up soon.

Frequently asked questions

Which is faster, a loop or a list comprehension?

A comprehension is usually a little faster and always shorter for building lists. For numeric work on big data, NumPy and pandas are far faster than either, because they run compiled code on whole arrays.

What does enumerate return?

An iterator of pairs (index, item). Wrap it in list() to see them all. It never builds a big list in memory.

Can I use zip with more than two lists?

Yes, as many as you like, as the three-list example showed. In Python 3.10 and newer, zip(a, b, strict=True) raises an error if the lengths differ, which is a useful safety check.

How do I loop over a list and remove items safely?

Create a new list with the items you want to keep (a filter), then assign it back. Alternatively loop over a copy: for x in items[:].

How can I find a pattern when I do not know where to start?

Describe the result in words: "a total", "a count of matches", "the largest and where it is", "each compared with the one before". Each phrase maps to one pattern in the table at the top of this lesson.

Do I need loops at all once I know pandas?

Yes, less often but still regularly: for calling APIs per row, producing one file per store, running simulations, and for the logic inside functions you pass to apply.

Newsletter

Keep learning Python

One email a week with Python and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.

Ready to practise? Try the Python interview questions