Skip to content

Python lesson 5 of 9

Python For Loops and Iteration

Learn Python for loops from the ground up - iterate over lists and strings, use range, enumerate and zip, and steer a loop with break and continue.

Published · Every example on this page was run before it was published.

A list can hold a hundred scores, and a string can hold a thousand characters, but so far the only way you have to look at them is one line of code per value. That does not scale, and it is not how programs are written. What you need is a way to say "do this same thing once for every value in here" — and that is exactly what a loop does. Loops are the point where your programs stop being lists of instructions and start being able to handle data of any size.

What a For Loop Is

A for loop takes a collection of values and runs the same block of code once for each of them, handing you one value at a time.

Think about handing out exam papers in a classroom. You do not write separate instructions for each student — you walk down the row and repeat one action, "give this person their paper", until the row runs out. You do not need to know in advance how many students are seated. The row itself tells you when to stop, because eventually there is nobody left.

A Python for loop works the same way. You point it at a collection, give the current value a name, and write the action once:

Python
chores = ["water plants", "wash dishes", "take out trash"]

for chore in chores:
    print("Today I need to:", chore)
Output
Today I need to: water plants
Today I need to: wash dishes
Today I need to: take out trash

Read that top line as a sentence: for each chore in chores. Three pieces are doing the work. chores is the collection being walked through. chore is the loop variable — a name you invent, which Python fills in with a different value on each pass. The colon ends the line, and the indented block beneath it is the body, the code that repeats. One trip through the body is called an iteration, so this loop runs three iterations.

The loop variable is an ordinary variable. You choose the name, and a clear singular name (chore for a list of chores) makes the body read like English. Anything a collection can be walked through value by value is called iterable, and lists, strings, and ranges are all iterable.

Loops shine when you need to build up an answer across many values. A variable that collects a result as the loop runs is called an accumulator:

Python
prices = [4.50, 2.25, 10.00]
total = 0

for price in prices:
    total = total + price

print("Items:", len(prices))
print("Total:", total)
Output
Items: 3
Total: 16.75

Notice that total is created before the loop. If it were created inside the body it would be reset to zero on every iteration and the final answer would just be the last price. The accumulator lives outside the loop; only the updating happens inside it.

Looping Over a String

A string is iterable too, and looping over one hands you a single character at a time, left to right:

Python
word = "loop"

for letter in word:
    print(letter)
Output
l
o
o
p

That is the foundation of nearly every text-processing task: counting, searching, cleaning, or translating character by character. Here is a counter that combines a loop with an if:

Python
name = "Priyanka"
vowel_count = 0

for character in name:
    if character.lower() in "aeiou":
        vowel_count += 1

print(name, "has", vowel_count, "vowels")
Output
Priyanka has 3 vowels

Two things are worth pausing on. character.lower() converts the character to lowercase so that a capital letter is still recognised — without it, the P and a capital A would be missed. And in "aeiou" asks whether the character appears anywhere in that string of vowels, which is far shorter than writing five separate comparisons. The += shorthand means "add to what is already there", so vowel_count += 1 is another way of writing vowel_count = vowel_count + 1.

Counting with range()

Sometimes you do not have a collection to walk through — you just want to repeat something a fixed number of times, or count through a sequence of numbers. range() produces exactly that: a stream of whole numbers, generated one at a time.

With a single argument, range(stop) counts from 0 up to but not including stop:

Python
for number in range(5):
    print(number)
Output
0
1
2
3
4

That is five numbers, starting at 0 and ending at 4. The rule that the stop value is left out is the single most important thing to remember about range(), and it trips up almost every beginner at least once. It exists for a good reason: because Python indexes lists starting at 0, range(len(items)) produces precisely the valid index positions of a list with no arithmetic on your part.

With two arguments you choose where the counting starts. range(start, stop) begins at start and still stops just before stop:

Python
for number in range(1, 6):
    print("Count:", number)
Output
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5

To count 1 through 5, you must write 6 as the stop value. A handy way to think about it: the number of iterations is stop - start, so range(1, 6) runs 6 - 1, or 5 times.

A third argument sets the step — how much to move each time. A step of 2 counts every other number, and a negative step counts downward:

Python
for even in range(0, 10, 2):
    print("Even:", even)

for seconds in range(3, 0, -1):
    print("T-minus", seconds)
Output
Even: 0
Even: 2
Even: 4
Even: 6
Even: 8
T-minus 3
T-minus 2
T-minus 1

Wrapping range() in list() is the fastest way to see exactly what it will produce, which is a good habit whenever you are unsure about the boundaries:

Python
print(list(range(5)))
print(list(range(2, 7)))
print(list(range(10, 0, -3)))
Output
[0, 1, 2, 3, 4]
[2, 3, 4, 5, 6]
[10, 7, 4, 1]

The last one steps down by 3 from 10 and stops before reaching 0, so 1 is the final value included.

When you only want to repeat an action and do not care about the number itself, the conventional loop variable name is a single underscore. It is a normal variable, but Python programmers read _ as "I am deliberately ignoring this value":

Python
for _ in range(3):
    print("beep")
Output
beep
beep
beep

enumerate() and zip()

Two built-in functions cover the situations a plain for loop handles awkwardly.

enumerate() is for when you need both the position and the value. It wraps the collection and hands back a pair on each iteration, which you unpack into two loop variables separated by a comma:

Python
colors = ["red", "green", "blue"]

for index, color in enumerate(colors):
    print(index, color)
Output
0 red
1 green
2 blue

Counting from 0 is usually what you want for indexing, but if the number is for a human to read, the start argument shifts it:

Python
podium = ["Ana", "Ben", "Chi"]

for position, name in enumerate(podium, start=1):
    print(position, name)
Output
1 Ana
2 Ben
3 Chi

Important: start changes only the number you are given, never which values are visited. The loop still walks the whole list.

zip() is for parallel iteration — walking two or more collections side by side, taking one value from each on every pass. It is the right tool when related data lives in separate lists:

Python
students = ["Ana", "Ben", "Chi"]
scores = [91, 78, 84]

for student, score in zip(students, scores):
    print(student, "scored", score)
Output
Ana scored 91
Ben scored 78
Chi scored 84

If the collections are different lengths, zip() stops as soon as the shortest one runs out, silently ignoring the leftovers. That is worth knowing before it surprises you:

Python
letters = ["a", "b", "c"]
numbers = [1, 2]

print(list(zip(letters, numbers)))
Output
[('a', 1), ('b', 2)]

Each pair is printed inside round brackets because zip() produces tuples — fixed groupings of values. Unpacking them into two loop variables, as in the earlier example, is the normal way to use them.

Steering a Loop: break, continue, and else

By default a loop visits every value. Two keywords change that.

break stops the loop immediately and jumps to the code after it. Use it when you have found what you were looking for and there is no reason to keep checking:

Python
numbers = [12, 7, 40, 3, 25]

for number in numbers:
    print("Checking", number)
    if number > 30:
        print("Found one over 30:", number)
        break
Output
Checking 12
Checking 7
Checking 40
Found one over 30: 40

The values 3 and 25 are never examined, because break ended the loop at 40.

continue skips the rest of the current iteration and moves straight to the next value. The loop keeps going — only this one pass is cut short. It is the natural way to filter out values you want to ignore:

Python
readings = [3, -1, 8, -4, 5]
total = 0

for reading in readings:
    if reading < 0:
        continue
    total += reading

print("Total of valid readings:", total)
Output
Total of valid readings: 16

Python also allows an else block attached to a loop, which confuses almost everyone the first time, because it has nothing to do with the else that follows an if. A loop's else runs when the loop finished normally — that is, it ran out of values without ever hitting break. Read it as "if no break happened". It is useful for search problems where you need to report failure:

Python
passwords = ["hunter2", "letmein", "qwerty"]
target = "correcthorse"

for password in passwords:
    if password == target:
        print("Found it")
        break
else:
    print("Never found", target)
Output
Never found correcthorse

Change target to "letmein" and the break fires, so the else is skipped entirely and only Found it is printed. Without this feature you would need a separate flag variable set to True when the match is found, then checked after the loop — for/else replaces that bookkeeping.

For Loops Versus While Loops

Python has a second kind of loop. A while loop repeats as long as a condition stays true, and it checks that condition before every pass:

Python
countdown = 3

while countdown > 0:
    print(countdown)
    countdown -= 1

print("Liftoff")
Output
3
2
1
Liftoff

The choice between the two comes down to one question: do you know in advance what you are stepping through? If you are walking a list, a string, or a fixed count of numbers, use a for loop — it cannot forget to advance, so it cannot run forever. Use a while loop when the stopping point depends on something computed as you go, and the number of repetitions is not known up front:

Python
balance = 100
days = 0

while balance > 10:
    balance = balance / 2
    days += 1

print("Days:", days)
print("Balance:", balance)
Output
Days: 4
Balance: 6.25

Nothing here says "repeat 4 times" — the loop discovers that number by halving until the condition fails. The danger of a while loop is the infinite loop: if you forget the line that moves toward the condition becoming false (here, changing balance), the program never stops. A for loop is immune to that mistake, which is why it should be your default.

A Worked Example

This small program builds a class report from two parallel lists, using zip(), enumerate(), continue, break, and a loop else together:

Python
students = ["Ana", "Ben", "Chi", "Dev", "Eli"]
scores = [88, 45, 92, 67, 100]

passing = []
total = 0

for rank, (name, score) in enumerate(zip(students, scores), start=1):
    if score < 60:
        print(rank, name, "needs a retake")
        continue
    passing.append(name)
    total += score
    print(rank, name, "scored", score)

print("Passing:", passing)
print("Average of passing scores:", total / len(passing))

for name, score in zip(students, scores):
    if score == 100:
        print("Perfect score by", name)
        break
else:
    print("Nobody scored 100")
Output
1 Ana scored 88
2 Ben needs a retake
3 Chi scored 92
4 Dev scored 67
5 Eli scored 100
Passing: ['Ana', 'Chi', 'Dev', 'Eli']
Average of passing scores: 86.75
Perfect score by Eli

Here is what each part does. The two lists hold related data in matching positions: scores[0] is Ana's score because students[0] is Ana. passing starts as an empty list and total starts at 0 — both are accumulators, created before the loop so they survive across iterations.

The loop header does two jobs at once. zip(students, scores) pairs each name with its score, and enumerate(..., start=1) numbers those pairs beginning at 1. The pattern for rank, (name, score) in unpacks the result in two stages: rank takes the number, and the brackets around name, score split the pair that zip() produced. The brackets matter — they tell Python that those two names together correspond to one item.

Inside the body, any score below 60 prints a retake message and then hits continue, which jumps to the next student without touching the accumulators. That is why Ben never appears in passing and his 45 never lands in total. Every other student is appended to passing, added into total, and printed.

After the loop, passing holds the four names that survived the filter, and total / len(passing) divides the accumulated score by how many names were kept, giving 347 divided by 4.

The second loop is a search. It walks the pairs again looking for a perfect score, and break ends it the moment Eli is found, so no further students are checked. The else attached to that loop would print the "Nobody scored 100" line only if the loop had finished without breaking. Since break ran, it is skipped.

Common Mistakes

Off-by-one from misreading range()

Python
for number in range(1, 5):
    print(number)
Output
1
2
3
4

The intention was to print 1 through 5, but 5 never appears. The stop value in range() is exclusive — counting halts just before it. The fix is range(1, 6). Whenever a loop is one iteration short or one too long, this is the first thing to check: for the numbers 1 through n, write range(1, n + 1).

Modifying a list while looping over it

Python
numbers = [1, 2, 2, 3, 2]

for number in numbers:
    if number == 2:
        numbers.remove(number)

print(numbers)
Output
[1, 3, 2]

Every 2 was supposed to disappear, and one survived. The reason is that the loop tracks its position by number, but removing an item shifts everything after it one place to the left — so the value that slid into the vacated slot gets skipped entirely. No error is raised, which makes this bug especially nasty. The fix is to never change a list you are iterating over. Build a new one instead:

Python
numbers = [1, 2, 2, 3, 2]
kept = []

for number in numbers:
    if number != 2:
        kept.append(number)

print(kept)
Output
[1, 3]

Reaching for range(len(x)) out of habit

Python
tools = ["hammer", "saw", "drill"]

for i in range(len(tools)):
    print(tools[i])
Output
hammer
saw
drill

This runs correctly, but it takes the long way round: it counts index numbers, then uses each one to look a value back up. When you only need the values, iterate over the collection directly. When you genuinely need the position too, use enumerate() — both versions below print the same three lines as above, with less to get wrong:

Python
tools = ["hammer", "saw", "drill"]

for tool in tools:
    print(tool)

for index, tool in enumerate(tools):
    print(index, tool)
Output
hammer
saw
drill
0 hammer
1 saw
2 drill

range(len(...)) is the right choice in one situation: when you need to assign back into the list by position, and even then enumerate() usually reads better.

Expecting the loop variable to change the list

Python
prices = [10, 20, 30]

for price in prices:
    price = price * 2

print(prices)
Output
[10, 20, 30]

The list is untouched. The loop variable price is a fresh name holding a copy of each value; reassigning it rebinds that name and has no effect on the list itself. On the next iteration, Python overwrites it with the next value anyway. To change the list, write into a position:

Python
prices = [10, 20, 30]

for index, price in enumerate(prices):
    prices[index] = price * 2

print(prices)
Output
[20, 40, 60]

Next Steps

Try the linked practice problem, two-number-sum. It hands you a list of numbers and a target, and asks you to find the pair that adds up to it — which is a loop inside a loop, with enumerate() keeping the positions straight so you never pair a number with itself, and break (or an early return) stopping the search the moment you have an answer. Every idea on this page shows up in it.

Before you start, spend a few minutes in the Python playground. Print list(range(...)) with a handful of different arguments until the exclusive stop value feels obvious rather than surprising, and try zip() on two lists of unequal length so the truncation is something you have seen for yourself.

Write it yourself

Reading about code and writing it are different skills. These exercises practise exactly what this lesson covered; they run in this tab and need no account.

When you want a harder one

Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.