Iteration is one of the foundational concepts in programming. At its core, iteration means executing a block of code repeatedly — either a fixed number of times or until a condition is met. Without loops, you would have to write the same logic over and over for every element in a dataset, which is neither practical nor maintainable.
In Python, you will reach for a loop whenever you need to:
Python provides two loop constructs: the for loop and the while loop. Alongside these, Python offers powerful built-in functions like enumerate(), zip(), and range() that make iteration concise and expressive. Let’s walk through each of these in depth.
A for loop iterates over a sequence — any iterable object such as a list, tuple, string, dictionary, set, or range. The loop runs once for each element in the sequence, binding the current element to a variable you define.
Syntax
for variable in iterable:
# do something with variable
The body of the loop is defined by indentation. Python does not use braces or keywords to delimit blocks — whitespace matters.
This is the most common use of a for loop. Each element in the list is visited exactly once, in order.
fruits = ["apple", "banana", "cherry", "mango"]
for fruit in fruits:
print(fruit)
Output:
apple banana cherry mango
Strings are sequences of characters, so a for loop walks through each character one at a time.
message = "Python"
for char in message:
print(f"Letter: {char}")
Output:
Letter: P Letter: y Letter: t Letter: h Letter: o Letter: n
The range() function generates a sequence of integers. It is the go-to tool when you need to loop a specific number of times or need numeric indices.
# range(stop) — 0 to stop-1
for i in range(5):
print(i) # 0, 1, 2, 3, 4
# range(start, stop) — start to stop-1
for i in range(2, 7):
print(i) # 2, 3, 4, 5, 6
# range(start, stop, step) — with a custom step
for i in range(0, 20, 3):
print(i) # 0, 3, 6, 9, 12, 15, 18
Notice that range() is exclusive of the stop value. This is by design and aligns with zero-based indexing. range(5) gives you exactly 5 elements: 0 through 4.
When you iterate over a dictionary directly, you get its keys. To access values or both keys and values, use the .values() or .items() methods.
profile = {"name": "Folau", "age": 30, "role": "Software Engineer"}
# Iterating over keys (default behavior)
for key in profile:
print(key)
# Iterating over values
for value in profile.values():
print(value)
# Iterating over key-value pairs — most useful in practice
for key, value in profile.items():
print(f"{key}: {value}")
Output of key-value iteration:
name: Folau age: 30 role: Software Engineer
Tuples and sets are both iterable. The key difference is that sets are unordered, so the iteration order is not guaranteed.
# Tuple
coordinates = (10, 20, 30)
for coord in coordinates:
print(coord)
# Set — order may vary between runs
languages = {"Python", "Java", "Go", "Rust"}
for lang in languages:
print(lang)
A while loop repeats a block of code as long as a condition evaluates to True. Use a while loop when you don’t know in advance how many iterations you need — the loop continues until the condition changes.
Syntax
while condition:
# loop body
Example — counting up
count = 0
while count < 5:
print(f"Count is {count}")
count += 1
Output:
Count is 0 Count is 1 Count is 2 Count is 3 Count is 4
The critical thing with while loops is that you must ensure the condition eventually becomes False. If it doesn't, you have an infinite loop, and your program will hang.
A classic use case for while loops is reading user input until a sentinel value is entered.
while True:
command = input("Enter a command (type 'quit' to exit): ")
if command.lower() == "quit":
print("Goodbye!")
break
print(f"You entered: {command}")
This pattern — while True with a break — is a clean way to handle input loops. The condition is always true, and the exit logic is handled explicitly inside the loop body.
These two statements give you fine-grained control over loop execution.
The break statement immediately terminates the innermost loop and resumes execution at the next statement after the loop.
numbers = [10, 25, 33, 47, 52, 68]
# Find the first number greater than 40
for num in numbers:
if num > 40:
print(f"Found it: {num}")
break
Output:
Found it: 47
The continue statement skips the rest of the current iteration and moves to the next one.
# Print only even numbers
for i in range(10):
if i % 2 != 0:
continue
print(i)
Output:
0 2 4 6 8
Use continue when you want to skip certain elements without restructuring your loop with deeply nested if blocks. It keeps the code flat and readable.
This is a Python-specific feature that surprises many developers coming from other languages. Both for and while loops can have an else block. The else block executes only if the loop completes normally — that is, without hitting a break.
For loop with else
teams = ["Lakers", "Jazz", "Suns"]
for team in teams:
print(team)
else:
print("All teams processed successfully.")
Output:
Lakers Jazz Suns All teams processed successfully.
Practical use — searching for an item
The else clause really shines when combined with break. Think of the else as "no break occurred":
target = "Celtics"
teams = ["Lakers", "Jazz", "Suns", "Warriors"]
for team in teams:
if team == target:
print(f"Found {target}!")
break
else:
print(f"{target} was not found in the list.")
Output:
Celtics was not found in the list.
If the break had been triggered, the else block would be skipped entirely. This eliminates the need for a separate "found" flag variable.
While loop with else
count = 0
while count < 5:
print(count)
count += 1
else:
print("Loop completed without break.")
You can place one loop inside another. The inner loop runs to completion for each iteration of the outer loop. Nested loops are common when working with multi-dimensional data or generating combinations.
Building a Multiplication Table
for i in range(1, 6):
for j in range(1, 6):
print(f"{i * j:4}", end="")
print() # new line after each row
Output:
1 2 3 4 5 2 4 6 8 10 3 6 9 12 15 4 8 12 16 20 5 10 15 20 25
Nested loop with break
Keep in mind that break only exits the innermost loop. If you need to break out of multiple levels, you'll need a flag variable or refactor the logic into a function with a return.
# Find a specific cell in a matrix
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
target = 5
found = False
for row_idx, row in enumerate(matrix):
for col_idx, value in enumerate(row):
if value == target:
print(f"Found {target} at row {row_idx}, col {col_idx}")
found = True
break
if found:
break
When you need both the index and the value during iteration, use enumerate(). This is far cleaner than manually tracking an index counter.
languages = ["Python", "Java", "Go", "Rust"]
# Without enumerate — avoid this pattern
index = 0
for lang in languages:
print(f"{index}: {lang}")
index += 1
# With enumerate — preferred approach
for index, lang in enumerate(languages):
print(f"{index}: {lang}")
# You can also set a custom start index
for index, lang in enumerate(languages, start=1):
print(f"{index}. {lang}")
Output (with start=1):
1. Python 2. Java 3. Go 4. Rust
Use enumerate() whenever you catch yourself creating a manual counter variable. It is more Pythonic and eliminates an entire category of off-by-one bugs.
The zip() function lets you iterate over two or more sequences simultaneously. It pairs up elements by position and stops when the shortest sequence is exhausted.
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
for name, score in zip(names, scores):
print(f"{name} scored {score}")
Output:
Alice scored 85 Bob scored 92 Charlie scored 78
Zipping three sequences
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
grades = ["B", "A", "C+"]
for name, score, grade in zip(names, scores, grades):
print(f"{name}: {score} ({grade})")
If the sequences have different lengths and you want to iterate to the longest one, use itertools.zip_longest():
from itertools import zip_longest
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92]
for name, score in zip_longest(names, scores, fillvalue="N/A"):
print(f"{name}: {score}")
Output:
Alice: 85 Bob: 92 Charlie: N/A
To traverse a sequence in reverse order without modifying the original, use the built-in reversed() function.
numbers = [1, 2, 3, 4, 5]
for num in reversed(numbers):
print(num)
Output:
5 4 3 2 1
You can also reverse a range:
for i in range(10, 0, -1):
print(i) # 10, 9, 8, ..., 1
Python offers a concise syntax for creating new lists (and other collections) from loops. These are called list comprehensions and they can replace simple for loops that build a list.
# Traditional for loop approach
squares = []
for x in range(1, 6):
squares.append(x ** 2)
print(squares) # [1, 4, 9, 16, 25]
# List comprehension — same result, one line
squares = [x ** 2 for x in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
# With a condition — only even squares
even_squares = [x ** 2 for x in range(1, 11) if x % 2 == 0]
print(even_squares) # [4, 16, 36, 64, 100]
Comprehensions also work with dictionaries and sets:
# Dictionary comprehension
word_lengths = {word: len(word) for word in ["Python", "Java", "Go"]}
print(word_lengths) # {'Python': 6, 'Java': 4, 'Go': 2}
# Set comprehension
unique_lengths = {len(word) for word in ["Python", "Java", "Go", "Rust"]}
print(unique_lengths) # {2, 4, 6}
Use comprehensions for simple transformations and filtering. If the logic gets complex with multiple conditions or side effects, a regular for loop is more readable.
expenses = [45.50, 120.00, 33.75, 89.99, 15.25]
total = 0
for expense in expenses:
total += expense
print(f"Total expenses: ${total:.2f}")
# Output: Total expenses: $304.49
# Pythonic alternative using built-in sum()
total = sum(expenses)
print(f"Total expenses: ${total:.2f}")
employees = [
{"name": "Alice", "department": "Engineering"},
{"name": "Bob", "department": "Marketing"},
{"name": "Charlie", "department": "Engineering"},
{"name": "Diana", "department": "Sales"}
]
target_name = "Charlie"
for employee in employees:
if employee["name"] == target_name:
print(f"Found {target_name} in {employee['department']}")
break
else:
print(f"{target_name} not found.")
size = 10
# Print header row
print(" ", end="")
for j in range(1, size + 1):
print(f"{j:4}", end="")
print()
print(" " + "-" * (size * 4))
# Print each row
for i in range(1, size + 1):
print(f"{i:3} |", end="")
for j in range(1, size + 1):
print(f"{i * j:4}", end="")
print()
tasks = []
while True:
action = input("Enter 'add', 'list', or 'quit': ").strip().lower()
if action == "quit":
print(f"Exiting. You have {len(tasks)} task(s).")
break
elif action == "add":
task = input("Enter task description: ").strip()
if task:
tasks.append(task)
print(f"Added: {task}")
else:
print("Task cannot be empty.")
elif action == "list":
if tasks:
for i, task in enumerate(tasks, start=1):
print(f" {i}. {task}")
else:
print("No tasks yet.")
else:
print("Unknown command. Try again.")
sales_data = {
"January": 15000,
"February": 18500,
"March": 22000,
"April": 19750,
"May": 24300
}
print("Monthly Sales Report")
print("=" * 30)
total_sales = 0
best_month = ""
best_amount = 0
for month, amount in sales_data.items():
print(f" {month:12s} ${amount:,.2f}")
total_sales += amount
if amount > best_amount:
best_amount = amount
best_month = month
print("=" * 30)
print(f" {'Total':12s} ${total_sales:,.2f}")
print(f"\nBest month: {best_month} (${best_amount:,.2f})")
Loop bodies in Python cannot be empty. If you need a placeholder — perhaps during development when you haven't written the logic yet — use pass.
for item in range(10):
pass # TODO: implement processing logic
This is also useful as a no-op in exception handling within loops:
data = ["10", "abc", "30", "xyz", "50"]
numbers = []
for item in data:
try:
numbers.append(int(item))
except ValueError:
pass # silently skip non-numeric values
print(numbers) # [10, 30, 50]
Forgetting to update the loop variable in a while loop is the most common source of infinite loops.
# BUG: count is never incremented — this runs forever
count = 0
while count < 10:
print(count)
# Missing: count += 1
# FIX: always ensure the condition variable changes
count = 0
while count < 10:
print(count)
count += 1
If you accidentally create an infinite loop in a terminal, press Ctrl + C to interrupt it.
Never add or remove elements from a list while iterating over it. This leads to skipped elements or index errors.
# BUG: modifying list during iteration
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:
if num % 2 == 0:
numbers.remove(num) # skips elements!
print(numbers) # [1, 3, 5] — looks correct but 4 was never checked
# FIX: iterate over a copy, or build a new list
numbers = [1, 2, 3, 4, 5, 6]
numbers = [num for num in numbers if num % 2 != 0]
print(numbers) # [1, 3, 5]
# Alternative: iterate over a copy using slicing
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers[:]: # numbers[:] creates a shallow copy
if num % 2 == 0:
numbers.remove(num)
print(numbers) # [1, 3, 5]
range() is exclusive of the stop value. This trips up developers who expect inclusive behavior.
# Want to print 1 through 5
for i in range(5):
print(i) # Prints 0, 1, 2, 3, 4 — NOT 1 through 5
# FIX: adjust start and stop
for i in range(1, 6):
print(i) # Prints 1, 2, 3, 4, 5
for loops when the number of iterations is known — iterating over a collection, a range, or any finite sequence.while loops for condition-based repetition — when you genuinely don't know how many iterations you need (user input, convergence algorithms, event polling).enumerate() over manual index tracking — it is more readable, less error-prone, and explicitly communicates your intent.zip() for parallel iteration — instead of indexing into multiple lists, zip them together for cleaner code.for student in students reads better than for s in students or for i in students.break and continue judiciously — they are powerful but can make flow harder to follow if overused. One break per loop is a reasonable guideline.break — useful for search patterns.range().