Python For Loop
A for loop lets you run a block of code once for every item in a collection — a list of names, the characters of a string, the lines of a file, or a range of numbers. Instead of manually tracking an index like you would in many other languages, Python’s for loop hands you each item directly, which makes code shorter, safer, and easier to read. It is the single most common way to process data in Python, so understanding exactly how it works — not just how to use it — pays off constantly.
Overview: How the For Loop Works
Python’s for loop is fundamentally a for-each loop. It does not count from a starting number to an ending number like a C-style for (i = 0; i < n; i++) loop. Instead, it walks over the elements produced by an iterable, one at a time, until the iterable is exhausted.
To understand what happens internally, you need two related concepts:
- Iterable — any object that can produce a sequence of values, because it implements a special method called
__iter__(). Lists, tuples, strings, dictionaries, sets, files, andrangeobjects are all iterables. - Iterator — the object actually doing the work of producing values one at a time. It implements
__next__(), which returns the next value each time it’s called, and raises a specialStopIterationexception when there are no values left.
When you write for item in iterable:, Python performs roughly these steps behind the scenes:
- Call
iter(iterable)to obtain an iterator object. - Call
next()on that iterator to get the next value. - Assign the returned value to the loop variable (
item) and execute the loop body. - Repeat step 2 and 3 until
next()raisesStopIteration. - Catch that
StopIterationsilently and exit the loop — you never see the exception yourself.
This is why almost anything can be looped over with for: as long as an object knows how to hand out its values one at a time, Python doesn’t care whether it’s backed by a list in memory, a file being read line by line, or numbers being generated on the fly (as with range or a generator function). The loop variable is an ordinary variable — after the loop ends, it still holds the last value it was assigned, and it can be unpacked into multiple names at once, which is how for key, value in some_dict.items(): works.
Python’s for loop also supports an optional else clause. The else block runs only if the loop finishes normally — that is, it was not stopped early by a break statement. This is a lesser-known but genuinely useful feature for “search and report” patterns, shown in Example 3 below.
Syntax
for item in iterable:
# loop body — runs once per item, with item bound to the current value
print(item)
else:
# optional — runs once, after the loop ends WITHOUT a break
print("Loop finished")
| Part | Meaning |
|---|---|
for |
Keyword that starts the loop |
item |
The loop (target) variable; can also be a tuple like a, b for unpacking |
in iterable |
Any iterable: list, tuple, string, dict, set, range, file, generator, etc. |
: |
Required; introduces the indented loop body |
break |
Exits the loop immediately, skipping the rest of the body and any else |
continue |
Skips the rest of the current iteration and moves to the next item |
else |
Optional block that runs only if the loop was not exited via break |
Two built-in functions appear alongside for constantly: range(start, stop, step), which generates a sequence of numbers without building a list in memory, and enumerate(iterable, start=0), which pairs each item with its index. There’s also zip() for looping over several sequences together, covered under Best Practices.
Examples
Example 1: Looping over a list
fruits = ["apple", "banana", "cherry", "date"]
for fruit in fruits:
print(f"I like {fruit}")
Output:
I like apple
I like banana
I like cherry
I like date
Each pass through the loop binds fruit to the next string in the list, in order, and the f-string embeds it directly into the printed message. No index variable is needed anywhere.
Example 2: range(), enumerate(), and accumulating a result
scores = [88, 92, 79, 95, 60]
total = 0
for index, score in enumerate(scores, start=1):
total += score
print(f"Student {index}: {score} points")
average = total / len(scores)
print(f"Average score: {average:.2f}")
Output:
Student 1: 88 points
Student 2: 92 points
Student 3: 79 points
Student 4: 95 points
Student 5: 60 points
Average score: 82.80
enumerate(scores, start=1) produces pairs like (1, 88), (2, 92), and so on, which the loop unpacks directly into index and score. Meanwhile total is a classic accumulator pattern: it starts at zero outside the loop and grows on each iteration, ending up with the sum used to compute the average after the loop finishes.
Example 3: Nested logic with break and for-else
def is_prime(n: int) -> bool:
if n < 2:
return False
for divisor in range(2, int(n ** 0.5) + 1):
if n % divisor == 0:
print(f"{n} is divisible by {divisor}")
break
else:
print(f"{n} is prime")
return True
return False
for number in [15, 17, 22, 23]:
is_prime(number)
Output:
15 is divisible by 3
17 is prime
22 is divisible by 2
23 is prime
The inner loop tests possible divisors up to the square root of n. If it finds one, it prints the divisor and immediately breaks — which skips the attached else block entirely. If the loop runs to completion without ever breaking (no divisor was found), the else block runs, reporting that the number is prime. This for...else pairing is the classic "search for something; if you never find it, do X" pattern, and it reads more cleanly than tracking a separate "found" boolean flag.
Under the Hood: Step by Step
You can watch the iterator protocol in action by driving it manually instead of letting for do it for you:
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
Output:
10
20
30
Here, iter(numbers) creates a fresh list-iterator object, and each call to next(iterator) advances it by one position, exactly as a for loop would do internally. If you called next(iterator) a fourth time, it would raise StopIteration — a normal for loop simply catches that exception for you and stops. This is also why you can only loop over a plain iterator (as opposed to a re-iterable container like a list) once: once it's exhausted, calling iter() on the same iterator object just returns itself, still empty.
Common Mistakes
Mistake 1: Modifying a list while iterating over it
numbers = [2, 4, 6, 8]
for n in numbers:
if n % 2 == 0:
numbers.remove(n)
print(numbers)
Output:
[4, 8]
This looks like it should empty the list, but it doesn't. Internally, the for loop tracks a numeric position and asks the list for the item at that position each time. Removing an item shifts every later item one slot to the left, so the loop's position skips over the very next element — the loop silently steps over values. The fix is to never mutate the list you're iterating over; instead, build a new list or iterate over a copy:
numbers = [2, 4, 6, 8]
numbers = [n for n in numbers if n % 2 != 0]
print(numbers)
Output:
[]
A list comprehension builds a brand-new list from scratch, so there's no shifting-index problem. If you must remove items in place, loop over an explicit copy instead, e.g. for n in numbers[:]:.
Mistake 2: Assuming range() is inclusive of its stop value
for i in range(1, 10):
print(i)
Output:
1
2
3
4
5
6
7
8
9
New Python developers often expect this to print through 10, since it "looks like" 1 to 10. But range(start, stop) always excludes stop — it generates values up to, but not including, that number. To print 1 through 10 inclusive, the stop value needs to be one higher:
for i in range(1, 11):
print(i)
Output:
1
2
3
4
5
6
7
8
9
10
A useful mental model: range(a, b) produces exactly b - a values, starting at a.
Best Practices
- Iterate directly over a collection (
for fruit in fruits:) instead of indexing withfor i in range(len(fruits)): fruits[i]— it's shorter and less error-prone. - When you need the index and the value, use
enumerate()rather than manually maintaining a counter variable. - Never add to or remove from a list while looping over that same list; build a new list (often with a comprehension) or loop over a copy (
list[:]) instead. - Use
zip(list_a, list_b)to loop over two or more sequences in parallel instead of indexing each one separately. - Reach for a
for...elsewhen your loop is a search: it removes the need for a manual "found" flag. - Prefer a list, set, or dict comprehension over a manual accumulator loop when you're just transforming or filtering data into a new collection — it's usually more readable and often faster.
- Give the loop variable a meaningful, singular name (
for student in students:, notfor x in students:) so the body reads naturally. - For very large or infinite sequences, prefer generators/iterators over building full lists up front, since
foronly ever needs one value at a time.
Practice Exercises
- Write a
forloop that prints the square of every integer from 1 to 10 (inclusive), one per line. - Given
words = ["kiwi", "pomegranate", "fig", "blueberry"], use aforloop withenumerate()to print each word's index and text, then determine and print which word is the longest. - Write a function that takes a list of numbers and uses a
for...elseloop to check whether any of them is negative. If it finds one, print the number andbreak; if it never finds one, let theelseclause print"No negative numbers found".
Summary
- Python's
forloop is a for-each loop: it walks over the values an iterable produces, rather than counting indices. - Under the hood,
forcallsiter()to get an iterator, then repeatedly callsnext()untilStopIterationis raised. range()generates numbers on demand and excludes itsstopvalue;enumerate()pairs items with their index.- An optional
elseclause runs only when the loop completes without hittingbreak— ideal for search patterns. - Never mutate a list while iterating over it directly; iterate over a copy or build a new collection instead.
- Prefer comprehensions and
enumerate()/zip()over manual index bookkeeping for cleaner, more idiomatic Python.
