Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python Module 3 – Control flow: decisions, loops and pattern matching

for loops with range, enumerate and zip

Loop over lists, strings, dictionaries and ranges with Python's for, number items with enumerate, pair lists with zip and catch missing data with strict=True.

  • Beginner
  • 18 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot

What you will learn

  • Loop over sequences, dictionaries and ranges
  • Use enumerate and zip instead of index arithmetic
  • Detect length mismatches with zip(strict=True)

Before you start

On this page

A Python for loop does not count anything itself. It takes the items of a collection one at a time: each character of a string, each name in a list, each key of a dictionary, and it works the same way for anything Python can iterate over. When you do need numbers, range() supplies them, and enumerate() and zip() cover the two jobs for which you would otherwise count positions by hand.

One item at a time

for name in collection: takes the first item, gives it the name name, runs the block, and repeats with the next item until there are none left. There is no counter to update and no condition to get wrong:

Strings, lists and dictionaries Python · each_item.py
for letter in "Go!":
    print(letter)

for city in ["Pune", "Lagos", "Lima"]:
    print(city.upper())

stock = {"pens": 12, "pads": 0, "clips": 40}
for item in stock:
    print("key:", item)
for item, count in stock.items():
    print(item, "->", count)
print("Total:", sum(stock.values()))

Output

G
o
!
PUNE
LAGOS
LIMA
key: pens
key: pads
key: clips
pens -> 12
pads -> 0
clips -> 40
Total: 52

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 each_item.py

A string gives its characters and a list its items, in order. A dictionary gives its keys, in the order they were added. To get each key with its value, loop over stock.items() and unpack the two parts into two names, as the second dictionary loop does; stock.values() gives the values alone.

break, continue and a loop’s else block work in a for loop as they do in the while loops of the previous lesson: break leaves the loop, continue moves on to the next item, and the else block runs when the items run out without a break, even when there were none at all.

Counting with range()

range(stop) gives the whole numbers from 0 up to, but not including, stop. range(start, stop) starts somewhere else, and range(start, stop, step) takes steps of any size but 0, also backwards:

range() in a loop and on its own Python · ranges.py
for n in range(3, 0, -1):
    print(n)
print("Go")

print(list(range(5)))
print(list(range(2, 11, 3)))
print(list(range(10, 0, -2)))
print(list(range(4, 4)))

seats = range(0, 1_000_000_000, 7)
print(len(seats), seats[-1], 700 in seats, 701 in seats)

Output

3
2
1
Go
[0, 1, 2, 3, 4]
[2, 5, 8]
[10, 8, 6, 4, 2]
[]
142857143 999999994 True False

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ranges.py

The countdown shows the rule that matters most: the stop value is never included. range(3, 0, -1) gives 3, 2 and 1, and range(4, 4) gives nothing at all, because there is no number from 4 up to but not including 4. A loop over an empty range simply does not run. range(1, 11) is the way to write “1 to 10”.

The calls to list() are there only to print the numbers. A range does not store them: it keeps its start, stop and step and works out each number when asked. So the range in the last line, with over 142 million numbers, takes no more memory than range(5), and len(), indexing (seats[-1] is the last one) and in all work on it without building a list.

Positions with enumerate()

When you need each item and its position, it is tempting to loop over range(len(items)) and look each item up. enumerate() hands you both at once:

Numbering a list Python · finish_order.py
# In the order they crossed the finish line
finishers = ["Meera", "Tom", "Ana", "Kofi"]

for i in range(len(finishers)):
    print(i + 1, finishers[i])

for place, name in enumerate(finishers, start=1):
    print(place, name)

Output

1 Meera
2 Tom
3 Ana
4 Kofi
1 Meera
2 Tom
3 Ana
4 Kofi

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 finish_order.py

Both loops print the same results table. The second has no i + 1 and no finishers[i], which are exactly the places where off-by-one mistakes creep in. enumerate(items) counts from 0 like positions do; start=1 makes it count the way people do.

Pairs with zip()

zip() walks through several lists side by side and gives one item from each in every pass. It is the way to combine lists that belong together, such as names and their marks:

A mark that is missing Python · missing_mark.py
names = ["Asha", "Ravi", "Mei", "Kiran"]
marks = [78, 91, 66]   # Kiran's mark was never entered

for name, mark in zip(names, marks):
    print(name, mark)
print(len(list(zip(names, marks))), "pairs for", len(names), "names")

for name, mark in zip(names, marks, strict=True):
    print(name, mark)

Output (exit status 1)

Asha 78
Ravi 91
Mei 66
3 pairs for 4 names
Asha 78
Ravi 91
Mei 66

Printed as an error (standard error)

Traceback (most recent call last):
  File "missing_mark.py", line 8, in <module>
    for name, mark in zip(names, marks, strict=True):
                      ~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: zip() argument 2 is shorter than argument 1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 missing_mark.py

Four names but three marks, and the first loop does not complain: by default zip() stops as soon as the shortest list runs out, so Kiran is silently left out. That default is useful when the lists may differ on purpose, and dangerous when they should match, because the missing data goes unnoticed.

strict=True turns the mismatch into an error: ValueError: zip() argument 2 is shorter than argument 1. Notice when the error came. The strict loop had already printed three pairs, because zip() only finds out that marks is shorter when it reaches the end. If nothing may happen before the check, compare the lengths first, or build the result completely, as in this lesson’s exercise, before you use any of it.

Version note

zip() gained strict in Python 3.10 (PEP 618). New in Python 3.14: map(), which calls a function with the items of one or more lists, accepts strict=True too.

Other orders without changing the data

reversed() and sorted() let a loop visit the items in another order while the list itself stays as it was:

Backwards and sorted Python · other_orders.py
queue = ["Tom", "Asha", "Mei"]

for name in reversed(queue):
    print("reversed:", name)

for name in sorted(queue):
    print("sorted:", name)

print("queue is still", queue)

Output

reversed: Mei
reversed: Asha
reversed: Tom
sorted: Asha
sorted: Mei
sorted: Tom
queue is still ['Tom', 'Asha', 'Mei']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 other_orders.py

sorted() makes a new, sorted list; reversed() reads the list from the end without copying it. Neither changes queue, as the last line shows.

Changing a collection while you loop over it

Behind the scenes, a for loop over a list moves through it by position: 0, then 1, then 2. Remove an item during the loop and the items after it move up one place, so the loop skips the one that took its place:

Removing items in a loop: the bug and two fixes Python · remove_in_loop.py
marks = [30, 20, 65, 34, 90]

# The bug: removing items from the list that the loop is reading
kept = marks.copy()
for m in kept:
    if m < 35:
        kept.remove(m)
print("Bug:     ", kept)

# Fix 1: loop over a copy and remove from the list itself
kept = marks.copy()
for m in kept.copy():
    if m < 35:
        kept.remove(m)
print("Copy:    ", kept)

# Fix 2: build a new list
passed = []
for m in marks:
    if m >= 35:
        passed.append(m)
print("New list:", passed)

Output

Bug:      [20, 65, 90]
Copy:     [65, 90]
New list: [65, 90]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 remove_in_loop.py

The bug leaves 20 in the list. When 30 was removed, 20 moved into position 0, which the loop had already visited, and the next pass looked at 65. There are two dependable fixes: loop over a copy and change the original, or build a new list with the items you want to keep, which is usually the clearer of the two.

Dictionaries are stricter. The documentation warns that adding or removing keys while you loop over a dictionary may raise RuntimeError or miss entries, and that is what happens here:

A dictionary that changes size Python · dict_resize.py
stock = {"pens": 12, "pads": 0, "clips": 40, "tape": 0}

for item in list(stock):
    if stock[item] == 0:
        del stock[item]
print(stock)

for item in stock:
    if item == "pens":
        stock["pencils"] = 5

Output (exit status 1)

{'pens': 12, 'clips': 40}

Printed as an error (standard error)

Traceback (most recent call last):
  File "dict_resize.py", line 8, in <module>
    for item in stock:
                ^^^^^
RuntimeError: dictionary changed size during iteration

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dict_resize.py

The first loop works because list(stock) copies the keys before the loop starts. The second adds a key to the dictionary it is reading, and Python stops it with RuntimeError at its next step.

The loop variable after the loop

The loop variable is an ordinary variable. After the loop it still holds the last item, and if the loop had nothing to go through, it was never set at all:

What is left after a loop Python · after_the_loop.py
for n in range(3):
    pass
print("n after the loop:", n)

for m in []:
    pass
print("m after the loop:", m)

Output (exit status 1)

n after the loop: 2

Printed as an error (standard error)

Traceback (most recent call last):
  File "after_the_loop.py", line 7, in <module>
    print("m after the loop:", m)
                               ^
NameError: name 'm' is not defined

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 after_the_loop.py

n is 2, the last number of range(3). The loop over the empty list never ran, so m does not exist and using it is a NameError. If you need a value from a loop that might not run, give the variable a value before the loop.

Python Online Compiler Loop over your own lists and dictionaries in a full editor, in your browser.

Key takeaways

  • for takes the items of any iterable one at a time: characters of a string, items of a list, keys of a dictionary (.items() for keys with values).
  • range(start, stop, step) never includes stop; it stores no numbers, yet supports len(), indexing and in.
  • Use enumerate(items, start=1) instead of range(len(items)), and zip() to walk lists side by side.
  • zip() stops at the shortest input without a word; zip(..., strict=True) raises ValueError when the lengths differ, but only after it has given every pair it could make.
  • Do not add or remove items of the collection you are looping over: loop over a copy or build a new list.
  • reversed() and sorted() change the order of a loop, not the data.

Exercise

Exercise · Easy · Python

Pair names with marks and keep a running total

Write two functions with for loops.

  • merge_marks(names, marks) takes a list of names and a list of marks in the same order and returns a dictionary from each name to its mark: merge_marks(["Asha", "Ravi"], [78, 91]) returns {"Asha": 78, "Ravi": 91}. Each name appears once. When one list is longer than the other, a name or a mark is missing: raise ValueError instead of returning a dictionary (zip(..., strict=True) raises it for you). Two empty lists give an empty dictionary.
  • running_total(nums) returns a new list in which each item is the sum of the numbers up to and including that position: running_total([3, 4, 5]) returns [3, 7, 12]. An empty list gives [], and nums itself must not change.

Starter code · marks.py

def merge_marks(names, marks):
    """Return a dict of each name's mark; ValueError if lengths differ."""
    result = {}
    return result


def running_total(nums):
    """Return a new list of the sums so far."""
    totals = []
    return totals
The sample tests · test_marks.py
from marks import merge_marks, running_total


def raises_value_error(names, marks):
    """True when merge_marks(names, marks) raises ValueError."""
    try:
        merge_marks(names, marks)
    except ValueError:
        return True
    return False


def test_merge():
    """pairs each name with its mark"""
    assert merge_marks(["Asha", "Ravi"], [78, 91]) == {"Asha": 78, "Ravi": 91}
    assert merge_marks(["Mei"], [66]) == {"Mei": 66}


def test_merge_empty():
    """gives an empty dictionary for two empty lists"""
    assert merge_marks([], []) == {}


def test_merge_mismatch():
    """raises ValueError when a name or a mark is missing"""
    assert raises_value_error(["Asha", "Ravi", "Kiran"], [78, 91])
    assert raises_value_error(["Asha"], [78, 91])
    assert raises_value_error([], [50])


def test_running_total():
    """adds up the numbers so far at each position"""
    assert running_total([3, 4, 5]) == [3, 7, 12]
    assert running_total([10]) == [10]
    assert running_total([5, -5, 2]) == [5, 0, 2]


def test_running_total_empty():
    """gives an empty list for an empty list"""
    assert running_total([]) == []


def test_running_total_keeps_input():
    """leaves the list it is given unchanged"""
    nums = [1, 2, 3]
    running_total(nums)
    assert nums == [1, 2, 3]
A hint

For merge_marks, start with an empty dictionary and fill it in a loop over zip(names, marks, strict=True): each pass gives you one name and its mark. For running_total, keep the sum so far in a variable that starts at 0, add each number to it, and append the sum to a new list after every addition.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 What does remove_in_loop.py print?

    What does this program print? Choose one answer.

    marks = [30, 20, 65, 34, 90]
    
    # The bug: removing items from the list that the loop is reading
    kept = marks.copy()
    for m in kept:
        if m < 35:
            kept.remove(m)
    print("Bug:     ", kept)
    
    # Fix 1: loop over a copy and remove from the list itself
    kept = marks.copy()
    for m in kept.copy():
        if m < 35:
            kept.remove(m)
    print("Copy:    ", kept)
    
    # Fix 2: build a new list
    passed = []
    for m in marks:
        if m >= 35:
            passed.append(m)
    print("New list:", passed)
    Show the answer to question 1

    Answer: it prints

    Bug:      [20, 65, 90]
    Copy:     [65, 90]
    New list: [65, 90]

    Removing 30 moves 20 into position 0, which the loop has already visited, so 20 is never tested. 34 is removed, because it was reached. Looping over a copy or building a new list both keep only 65 and 90.

  2. Question 2 of 5 Which numbers does range(2, 11, 3) give?

    Choose one answer.

    Show the answer to question 2

    Answer: 2, 5 and 8

    It starts at 2 and adds 3 each time: 2, 5, 8. The next number would be 11, and the stop value is never included.

  3. Question 3 of 5 What does this print?

    Read the code, then choose one answer.

    print(list(zip("abc", [1, 2])))
    Show the answer to question 3

    Answer: [('a', 1), ('b', 2)]

    A string is iterable too, so zip() pairs its characters with the numbers and stops when the shorter input runs out: 'c' is dropped without a word. Only strict=True would raise ValueError.

  4. Question 4 of 5 prices = {"tea": 30, "toast": 45}. What does for x in prices: give x on its first pass?

    Choose one answer.

    Show the answer to question 4

    Answer: 'tea'

    Looping over a dictionary gives its keys, in the order they were added. Use prices.items() for pairs of a key and its value, and prices.values() for the values.

  5. Question 5 of 5 How many numbers does range(0, 100, 7) give?

    Type a number.

    Show the answer to question 5

    Answer: 15 numbers

    0, 7, 14 and so on up to 98, which is 7 × 14: that is 15 numbers, from 7 × 0 to 7 × 14. 105 would be next, but it is not below 100. len(range(0, 100, 7)) gives the same answer without building a list.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.