12. Iteration with for Loops and range()
In Chapter 11, we learned about while loops, which repeat actions as long as a condition remains true. While while loops are powerful and flexible, they require us to manually manage loop counters and update conditions. Python provides another type of loop—the for loop—that excels at a different but extremely common task: processing each item in a collection of data, one at a time.
The for loop is one of Python's most frequently used features. Whether you're processing lines in a file, calculating statistics from a sequence of numbers, or checking each character in a string, the for loop provides a clean, readable way to work through sequences of data. In this chapter, we'll explore how for loops work, how to use Python's range() function for counting operations, and how to control loop execution with break, continue, and the lesser-known else clause.
12.1) The for Loop and Iteration Concepts
12.1.1) What Is a for Loop?
A for loop in Python repeats a block of code once for each item in a sequence. The sequence could be a string, a list (which we'll learn about in detail in Chapter 14), or any other collection of items. The key difference from a while loop is that a for loop automatically handles the iteration—you don't need to manually update a counter or check a condition.
Here's the basic structure of a for loop:
for variable in sequence:
# Code block to execute for each item
# The variable holds the current itemThe for loop works like this:
- Python takes the first item from the sequence and assigns it to the variable
- The code block (indented under the
forstatement) executes - Python takes the next item, assigns it to the variable, and executes the block again
- This continues until all items in the sequence have been processed
Let's see a simple example with a string:
# Print each character in a name
name = "Alice"
for character in name:
print(character)Output:
A
l
i
c
eIn this example, the string "Alice" is the sequence. The loop processes each character one at a time. During the first iteration, character holds 'A', during the second iteration it holds 'l', and so on. The variable name character is our choice—we could have called it letter, char, or even just c. Choose names that make your code's purpose clear.
Loop Variable Naming Conventions: When writing for loops, choose variable names that describe what each item represents. Use descriptive names like character, student, or score when the value matters for understanding your code. Use i for simple numeric counters when you're just counting iterations. Use _ (underscore) when you don't need the value at all—this is a Python convention meaning "I don't care about this value."
Here's a practical example that counts vowels in a word:
# Count vowels in a word
word = "Python"
vowel_count = 0
for letter in word:
if letter in "aeiouAEIOU":
vowel_count += 1
print(f"Found vowel: {letter}")
print(f"Total vowels: {vowel_count}")Output:
Found vowel: o
Total vowels: 1The loop examines each letter in "Python". When it finds a vowel (using the in operator we learned in Chapter 7), it increments the counter and prints a message. This pattern—initializing a counter before the loop, then updating it inside—is extremely common in programming.
Here's another example that demonstrates accumulation by building a result:
# Count uppercase letters in a string
text = "Python Programming"
uppercase_count = 0
for char in text:
if char.isupper():
uppercase_count += 1
print(f"Found {uppercase_count} uppercase letters in '{text}'")Output:
Found 2 uppercase letters in 'Python Programming'This example shows the accumulation pattern: we initialize a counter to zero before the loop, then examine each character and increment the counter when we find an uppercase letter. After the loop completes, we use the accumulated result. This pattern appears constantly in programming—counting items that meet certain criteria, summing values, or building up results piece by piece.
12.1.2) Understanding Iteration
Iteration is the process of repeating an action for each element in a collection. When we say an object is iterable, we mean Python can process its elements one at a time in a for loop. Strings are iterable—we can loop through their characters. As we'll see shortly, many other Python types are also iterable.
The variable in a for loop (like character or letter in our examples) is called the loop variable. This variable is automatically created by the for loop and exists only within the loop's scope. Each time through the loop, Python assigns the next item from the sequence to this variable.
Here's an example that demonstrates how the loop variable changes:
# Show how the loop variable updates
colors = "RGB" # Red, Green, Blue
for color_code in colors:
print(f"Processing color code: {color_code}")
print(f" This is iteration for '{color_code}'")Output:
Processing color code: R
This is iteration for 'R'
Processing color code: G
This is iteration for 'G'
Processing color code: B
This is iteration for 'B'Each time through the loop, color_code holds a different character from the string. The loop automatically moves to the next character—we don't need to write any code to advance through the sequence.
12.1.3) The for Loop vs while Loop for Iteration
We could accomplish the same character-by-character processing with a while loop, but it requires more code and manual management:
# Using while loop to iterate over a string (more complex)
name = "Alice"
index = 0
while index < len(name):
character = name[index]
print(character)
index += 1 # Must manually update the indexThis produces the same output as our earlier for loop example, but notice what we had to do:
- Create and initialize an index variable
- Check the condition
index < len(name)on each iteration - Manually extract the character using
name[index] - Remember to increment the index (forgetting this creates an infinite loop!)
The for loop handles all of this automatically:
# Using for loop to iterate over a string (simpler)
name = "Alice"
for character in name:
print(character)This is much cleaner and less error-prone. The for loop is the natural choice when you want to process each item in a collection. Use a while loop when you need more control over the iteration logic or when you don't know in advance how many times to repeat.
12.2) Using range() for Counting Loops
12.2.1) Introducing the range() Function
While for loops excel at processing existing sequences, we often need to repeat an action a specific number of times—like printing a message five times or calculating the first ten square numbers. Python's range() function generates a sequence of numbers that we can iterate over with a for loop.
The simplest form of range() takes one argument—the number of integers to generate, starting from 0:
# Print numbers from 0 to 4
for number in range(5):
print(number)Output:
0
1
2
3
4⚠️ Common Mistake: Beginners often expect range(5) to produce 1, 2, 3, 4, 5, but it actually produces 0, 1, 2, 3, 4. Remember: range(n) starts at 0 and stops before n. This "stop before" behavior is consistent with Python's slicing, which we learned about in Chapter 5. The range starts at 0 (by default) and goes up to, but not including, the stop value.
This pattern is perfect for repeating an action a specific number of times:
# Print a greeting five times
for i in range(5):
print(f"Welcome! (iteration {i})")Output:
Welcome! (iteration 0)
Welcome! (iteration 1)
Welcome! (iteration 2)
Welcome! (iteration 3)
Welcome! (iteration 4)The variable i is a common name for a loop counter (short for "index" or "iteration"), though you can use any valid variable name. When the loop variable's value doesn't matter for your logic, i is a conventional choice.
12.2.2) Specifying Start and Stop Values
You can provide two arguments to range() to specify both where to start and where to stop:
# Print numbers from 1 to 5
for number in range(1, 6):
print(number)Output:
1
2
3
4
5Here, range(1, 6) starts at 1 and stops before 6, giving us the numbers 1 through 5. This is useful when you need to count starting from a value other than zero.
Let's use this to calculate a simple multiplication table:
# Print the 7 times table from 1 to 10
multiplier = 7
for number in range(1, 11):
result = multiplier * number
print(f"{multiplier} × {number} = {result}")Output:
7 × 1 = 7
7 × 2 = 14
7 × 3 = 21
7 × 4 = 28
7 × 5 = 35
7 × 6 = 42
7 × 7 = 49
7 × 8 = 56
7 × 9 = 63
7 × 10 = 70The loop runs from 1 to 10, multiplying each number by 7. This demonstrates how range() makes it easy to perform calculations over a specific numeric sequence.
12.2.3) Using a Step Value
The range() function accepts a third optional argument: the step value, which determines how much to increase (or decrease) between numbers:
# Print even numbers from 0 to 10
for number in range(0, 11, 2):
print(number)Output:
0
2
4
6
8
10With range(0, 11, 2), we start at 0, stop before 11, and increase by 2 each time. This gives us all even numbers from 0 to 10.
You can also use a negative step to count backwards:
# Countdown from 10 to 1
for number in range(10, 0, -1):
print(number)
print("Liftoff!")Output:
10
9
8
7
6
5
4
3
2
1
Liftoff!Here, range(10, 0, -1) starts at 10, stops before 0, and decreases by 1 each time. The negative step makes the range count backwards.
Let's see a practical example that calculates the sum of all odd numbers from 1 to 100:
# Sum all odd numbers from 1 to 100
total = 0
for number in range(1, 101, 2): # Start at 1, step by 2
total += number
print(f"Sum of odd numbers from 1 to 100: {total}")Output:
Sum of odd numbers from 1 to 100: 2500By using range(1, 101, 2), we generate only the odd numbers (1, 3, 5, ..., 99), avoiding the need to check each number's parity inside the loop. This makes the code more efficient and clearer in intent.
12.2.4) What range() Actually Returns
The range() function doesn't create a list of numbers in memory—it creates a range object that generates numbers on demand. This is memory-efficient, especially for large ranges:
# range() returns a range object, not a list
numbers = range(1000000)
print(type(numbers)) # Output: <class 'range'>
print(numbers) # Output: range(0, 1000000)Output:
<class 'range'>
range(0, 1000000)Even though this range represents a million numbers, it uses very little memory because Python doesn't actually create all million numbers until you iterate over them. Each number is generated only when needed by the loop.
If you need an actual list of numbers, you can convert the range:
# Convert range to a list
small_numbers = list(range(5))
print(small_numbers) # Output: [0, 1, 2, 3, 4]Output:
[0, 1, 2, 3, 4]We'll learn more about lists and the list() function in Chapter 14. For now, just know that range() works perfectly with for loops without any conversion.
12.3) Iterating Over Strings and Other Sequences
12.3.1) Iterating Over Strings Character by Character
We've already seen several examples of iterating over strings. This is one of the most common uses of for loops because strings are sequences of characters, and we often need to examine or process each character individually.
Here's an example that validates a password by checking that it contains at least one digit:
# Check if a password contains at least one digit
password = "secure123"
has_digit = False
for character in password:
if character.isdigit():
has_digit = True
print(f"Found digit: {character}")
if has_digit:
print("Password contains at least one digit ✓")
else:
print("Password must contain at least one digit ✗")Output:
Found digit: 1
Found digit: 2
Found digit: 3
Password contains at least one digit ✓The loop examines each character using the .isdigit() string method (which we learned in Chapter 5). When it finds a digit, it sets has_digit to True. After the loop completes, we check the flag to determine if any digits were found.
Here's another practical example that counts different types of characters:
# Analyze character types in a string
text = "Hello, World! 123"
letters = 0
digits = 0
spaces = 0
other = 0
for char in text:
if char.isalpha():
letters += 1
elif char.isdigit():
digits += 1
elif char.isspace():
spaces += 1
else:
other += 1
print(f"Letters: {letters}")
print(f"Digits: {digits}")
print(f"Spaces: {spaces}")
print(f"Other: {other}")Output:
Letters: 10
Digits: 3
Spaces: 2
Other: 2This loop categorizes each character using string methods we learned in Chapter 5. The if-elif-else chain (from Chapter 8) ensures each character is counted in exactly one category.
12.3.2) Processing Strings with Indices
Sometimes you need both the character and its position in the string. You can use range(len(string)) to iterate over the indices:
# Find positions of a specific character
text = "Mississippi"
search_char = "s"
print(f"Looking for '{search_char}' in '{text}':")
for index in range(len(text)):
if text[index] == search_char:
print(f" Found at index {index}")Output:
Looking for 's' in 'Mississippi':
Found at index 2
Found at index 3
Found at index 5
Found at index 6The loop iterates over indices from 0 to len(text) - 1. For each index, we check if the character at that position matches our search character. This approach is useful when you need to know where something appears, not just that it appears.
12.3.3) Iterating Over Other Sequence Types
The for loop works with any iterable sequence in Python. While we've focused on strings in this chapter, you'll learn about other sequence types in later chapters. For example, Chapter 14 will teach you about lists, which are ordered collections that can hold multiple values of any type. The for loop syntax remains the same regardless of what type of sequence you're iterating over—the loop automatically handles getting each item from the sequence.
12.4) Using break and continue in for Loops
12.4.1) The break Statement in for Loops
Just as with while loops (Chapter 11), the break statement immediately exits a for loop, skipping any remaining iterations. This is useful when you've found what you're looking for and don't need to continue searching.
Here's an example that searches for a specific character:
# Search for the first vowel in a string
text = "Python"
found_vowel = False
for char in text:
if char.lower() in "aeiou":
print(f"First vowel found: {char}")
found_vowel = True
break # Stop searching once we find the first vowel
if not found_vowel:
print("No vowels found")Output:
First vowel found: oWithout break, the loop would continue examining all remaining characters even after finding the first vowel. The break statement makes the code more efficient by stopping as soon as the task is complete.
Here's a practical example that validates user input by checking for invalid characters:
# Check if a username contains only allowed characters
username = "alice_123"
allowed = "abcdefghijklmnopqrstuvwxyz0123456789_"
is_valid = True
for char in username:
if char.lower() not in allowed:
print(f"Invalid character found: '{char}'")
is_valid = False
break # No need to check further
if is_valid:
print(f"Username '{username}' is valid ✓")
else:
print(f"Username '{username}' is invalid ✗")Output:
Username 'alice_123' is valid ✓The loop checks each character against the allowed set. If it finds an invalid character, it reports the problem and breaks out of the loop immediately—there's no point checking the rest of the username once we know it's invalid.
12.4.2) The continue Statement in for Loops
The continue statement skips the rest of the current iteration and moves to the next item in the sequence. This is useful when you want to skip certain items without exiting the loop entirely.
Here's an example that processes only certain characters:
# Print only consonants from a string
word = "Programming"
for letter in word:
if letter.lower() in "aeiou":
continue # Skip vowels
print(letter, end="")
print() # New line at the endOutput:
PrgrmmngWhen the loop encounters a vowel, continue skips the print() statement and moves to the next character. Only consonants reach the print statement.
Here's a more practical example that calculates statistics while skipping invalid data. Note that the .split() method (from Chapter 6) returns a list of strings, which we'll learn about in Chapter 14. For now, just know that for loops can iterate over the result:
# Calculate average of valid test scores (0-100)
# The .split() method returns a list of strings (Chapter 14)
scores_input = "85 92 -5 78 105 90 88"
valid_scores = 0
total = 0
for score_str in scores_input.split():
score = int(score_str)
if score < 0 or score > 100:
print(f"Skipping invalid score: {score}")
continue # Skip this score
valid_scores += 1
total += score
if valid_scores > 0:
average = total / valid_scores
print(f"Average of {valid_scores} valid scores: {average:.1f}")
else:
print("No valid scores to average")Output:
Skipping invalid score: -5
Skipping invalid score: 105
Average of 5 valid scores: 86.6The loop processes each score string. When it encounters an invalid score (negative or over 100), it prints a warning and uses continue to skip adding that score to the total. This allows the loop to process all valid scores while ignoring invalid ones.
12.4.3) When to Use break and continue
Use break when:
- You're searching for something and want to stop once you find it
- You've encountered an error condition that makes continuing pointless
- You've completed your task and don't need to process remaining items
Use continue when:
- You want to skip certain items based on a condition
- You're filtering data and want to process only items that meet criteria
- You want to avoid deeply nested
ifstatements by handling special cases early
Both break and continue can make your code clearer by explicitly showing when and why you're changing the normal flow of iteration. However, overusing them can make code harder to follow—use them when they genuinely improve clarity.
12.5) Using else with for Loops
12.5.1) The for-else Pattern
Python's for loops support an optional else clause that executes after the loop completes normally—that is, when the loop finishes iterating through all items without encountering a break statement. This might seem strange at first (why "else" if there's no "if"?), but it's useful for distinguishing between "found what I was looking for" and "searched everything and didn't find it."
Here's the basic structure:
for item in sequence:
# Loop body
if some_condition:
break
else:
# This executes only if the loop completed without breaking
print("Loop completed normally")The else block runs if and only if the loop exits naturally by exhausting the sequence. If a break statement exits the loop early, the else block is skipped.
Let's see a practical example that searches for a specific value. The .split() method (from Chapter 6) returns a list of strings, which we'll learn about in Chapter 14:
# Search for a target number
numbers = "2 4 6 8 10"
target = 7
for num_str in numbers.split():
num = int(num_str)
if num == target:
print(f"Found {target}!")
break
else:
print(f"{target} not found in the sequence")Output:
7 not found in the sequenceThe loop searches through all numbers. Since it never finds 7, it completes normally and the else block executes. Now let's search for a number that exists:
# Search for a target number that exists
numbers = "2 4 6 8 10"
target = 6
for num_str in numbers.split():
num = int(num_str)
if num == target:
print(f"Found {target}!")
break
else:
print(f"{target} not found in the sequence")Output:
Found 6!This time, when the loop finds 6, it executes break, which skips the else block entirely. This pattern elegantly handles both the "found" and "not found" cases without needing a separate flag variable.
12.5.2) Practical Uses of for-else
The for-else pattern is particularly useful for search operations. Here's an example that validates whether a string contains only digits:
# Check if a string is a valid integer
user_input = "12345"
for char in user_input:
if not char.isdigit():
print(f"Invalid: '{char}' is not a digit")
break
else:
print(f"'{user_input}' is a valid integer")Output:
'12345' is a valid integerIf the input contains any non-digit character, the loop breaks and the else block doesn't run. If all characters are digits, the loop completes normally and the else block confirms validity.
Let's test with invalid input:
# Check if a string is a valid integer (invalid case)
user_input = "123a5"
for char in user_input:
if not char.isdigit():
print(f"Invalid: '{char}' is not a digit")
break
else:
print(f"'{user_input}' is a valid integer")Output:
Invalid: 'a' is not a digit12.5.3) Comparing for-else with Flag Variables
Before learning about for-else, you might have used a flag variable to track whether something was found:
# Using a flag variable (traditional approach)
text = "Python"
found_vowel = False
for char in text:
if char.lower() in "aeiou":
print(f"Found vowel: {char}")
found_vowel = True
break
if not found_vowel:
print("No vowels found")Output:
Found vowel: oThe for-else pattern eliminates the need for the flag variable:
# Using for-else (more Pythonic)
text = "Python"
for char in text:
if char.lower() in "aeiou":
print(f"Found vowel: {char}")
break
else:
print("No vowels found")Output:
Found vowel: oBoth approaches work correctly, but the for-else version is more concise and clearly expresses the intent: "search for something, and if you don't find it, do this." The else clause directly represents the "not found" case.
12.6) Choosing Between for and while Loops
12.6.1) When to Use a for Loop
Use a for loop when:
1. You're processing each item in a collection:
# Processing each character in a string
message = "Hello"
for char in message:
print(char.upper())Output:
H
E
L
L
O2. You need to repeat an action a specific number of times:
# Print a border line
for i in range(40):
print("-", end="")
print()Output:
----------------------------------------3. You're iterating over a numeric range:
# Calculate factorial of 5
factorial = 1
for n in range(1, 6):
factorial *= n
print(f"5! = {factorial}")Output:
5! = 1204. You know in advance what you're iterating over:
# Process a known sequence of values
grades = "A B C D F"
for grade in grades.split():
print(f"Grade: {grade}")Output:
Grade: A
Grade: B
Grade: C
Grade: D
Grade: FThe key characteristic of for loops is that they work with definite iteration—you know what sequence you're iterating over, even if you don't know in advance how many items it contains.
12.6.2) When to Use a while Loop
Use a while loop when:
1. You're repeating until a condition changes:
# Keep asking until valid input is received
while True:
age_input = input("Enter your age: ")
if age_input.isdigit():
age = int(age_input)
if age > 0:
print(f"Age recorded: {age}")
break
print("Please enter a valid positive number")2. You don't know how many iterations you'll need:
# Count how many times you can double a number before exceeding 1000
number = 1
count = 0
while number <= 1000:
number *= 2
count += 1
print(f"Doubled {count} times to reach {number}")Output:
Doubled 10 times to reach 10243. The iteration depends on complex conditions:
# Simulate a simple game where player health decreases
health = 100
turn = 0
while health > 0 and turn < 10:
damage = 15
health -= damage
turn += 1
print(f"Turn {turn}: Health = {health}")
if health <= 0:
print("Game over!")
else:
print("Survived 10 turns!")Output:
Turn 1: Health = 85
Turn 2: Health = 70
Turn 3: Health = 55
Turn 4: Health = 40
Turn 5: Health = 25
Turn 6: Health = 10
Turn 7: Health = -5
Game over!4. You need more control over the iteration logic:
# Process a string but skip consecutive duplicates
text = "bookkeeper"
index = 0
while index < len(text):
char = text[index]
print(char, end="")
# Skip consecutive identical characters
while index < len(text) and text[index] == char:
index += 1
print()Output:
bokeperThe key characteristic of while loops is indefinite iteration—you continue until a condition becomes false, but you might not know in advance when that will happen.
12.6.3) Converting Between for and while
Many problems can be solved with either loop type. Here's the same task implemented both ways:
# Using for loop: Sum numbers from 1 to 10
total = 0
for number in range(1, 11):
total += number
print(f"Sum (for loop): {total}")
# Using while loop: Sum numbers from 1 to 10
total = 0
number = 1
while number <= 10:
total += number
number += 1
print(f"Sum (while loop): {total}")Output:
Sum (for loop): 55
Sum (while loop): 55Both produce the same result, but the for loop is more concise and less error-prone—there's no risk of forgetting to increment the counter. When you can use either, prefer the for loop.
However, some problems are more naturally expressed with while:
# Find the first power of 2 greater than 1000
power = 1
exponent = 0
while power <= 1000:
exponent += 1
power = 2 ** exponent
print(f"2^{exponent} = {power} (first power of 2 > 1000)")Output:
2^10 = 1024 (first power of 2 > 1000)This problem doesn't fit naturally into a for loop because we don't know in advance how many iterations we need—we're searching for a value that meets a condition.
In this chapter, we've explored Python's for loop, which provides a clean and powerful way to iterate over sequences. We learned how range() generates numeric sequences for counting operations, how to control loop flow with break and continue, and how the for-else pattern elegantly handles search operations. We also examined when to choose for loops versus while loops based on the nature of your iteration task.
The for loop is one of Python's most frequently used features. As you continue learning Python, you'll use for loops constantly—processing data from files, working with lists and dictionaries, and transforming collections of information. In the next chapter, we'll explore Python's match statement, which provides another way to make decisions based on values, offering a more structured alternative to long if-elif chains for certain types of problems.