Python Loops and Iteration: The Complete Beginner's Guide

Conditionals choose a path; loops repeat a path. This chapter covers \`for\` and \`while\`, practical use of \`range()\`, \`enumerate()\`, and \`zip()\`, plus break/continue, loop else, and nested-loop patterns for real collections.

Chapter 9 of 20 · Beginner · 45 min · Python Programming Course

In Day 8 you learned how to make programs choose a path. Now you will learn how to make programs repeat a path — automatically, reliably, and without writing the same code multiple times.

Loops are what turn a 5-line script into a program that processes 50,000 records. The same code that prints one name can print ten thousand names. The same logic that validates one form can validate every form in a database. That scalability — doing something once and having Python repeat it as many times as needed — is one of the most powerful ideas in all of programming.

Python has two loop types. The for loop iterates over a sequence of items — a list, a string, a range of numbers, a dictionary. The while loop repeats as long as a condition remains True. Together they handle every repetition pattern you will encounter.

What You Will Learn in This Chapter

By the end of this tutorial you will be able to:

  • Write for loops to iterate over lists, strings, tuples, and dictionaries
  • Use range() to generate number sequences with full control
  • Use enumerate() for index-aware iteration
  • Use zip() to iterate over multiple sequences simultaneously
  • Write while loops with correct termination conditions
  • Control loop execution with break, continue, and pass
  • Use else clauses on loops
  • Write nested loops for 2D data
  • Use loop patterns: accumulation, searching, filtering, transformation
  • Avoid infinite loops and other common loop mistakes

Estimated time: 45 minutes reading + 25 minutes practice

The for Loop — Iterating Over a Sequence

A for loop takes each item from a sequence one at a time and runs the loop body with that item. When the sequence is exhausted, the loop ends:

fruits = ["apple", "banana", "cherry", "mango"]

for fruit in fruits:
    print(fruit)

Output:

apple
banana
cherry
mango

The variable fruit is created by the loop — you name it whatever makes sense for the data. Each iteration Python assigns the next item to that variable and runs the indented block. When the list runs out, execution continues after the loop.

# The loop variable name is your choice
for name in ["Alice", "Bob", "Carol"]:
    print(f"Hello, {name}!")

for number in [10, 20, 30, 40]:
    print(number * 2)

for character in "Python":
    print(character)

Output of the last loop:

P
y
t
h
o
n

Strings are sequences of characters — for loops over them character by character. The same loop syntax works on any sequence: lists, tuples, strings, sets, dictionaries, and anything else Python considers iterable.

range() — Generating Number Sequences

range() generates a sequence of integers without creating them all in memory at once. It is the standard way to loop a specific number of times or to work with numeric indexes.

range(stop) — From 0 to stop-1

for i in range(5):
    print(i)
# 0, 1, 2, 3, 4

range(5) generates 0, 1, 2, 3, 4 — five numbers starting from 0. The stop value is always excluded.

range(start, stop) — From start to stop-1

for i in range(1, 6):
    print(i)
# 1, 2, 3, 4, 5

for i in range(5, 10):
    print(i)
# 5, 6, 7, 8, 9

range(start, stop, step) — With a Step Size

# Count by 2s
for i in range(0, 11, 2):
    print(i)
# 0, 2, 4, 6, 8, 10

# Count backwards
for i in range(10, 0, -1):
    print(i)
# 10, 9, 8, 7, 6, 5, 4, 3, 2, 1

# Every third number
for i in range(0, 30, 3):
    print(i)
# 0, 3, 6, 9, 12, 15, 18, 21, 24, 27

Converting range() to a List

range() is not a list — it is a lazy sequence that generates values on demand. Convert it explicitly when you need a list:

print(range(5))           # range(0, 5) — not a list
print(list(range(5)))     # [0, 1, 2, 3, 4]
print(list(range(1, 11))) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Using range() to Loop N Times

The most common use — run something exactly N times:

# Print a separator 30 times
print("-" * 30)   # string repetition is faster for this specific case

# But for N arbitrary repetitions:
for _ in range(5):
    print("Processing...")

The _ variable name signals that the loop variable is not used — you just need the loop to run N times.

enumerate() — Loops With Index and Value Together

enumerate() wraps any iterable and yields both the index and the value at each step. This eliminates the need for a separate counter variable:

languages = ["Python", "JavaScript", "Java", "Go", "Rust"]

for index, language in enumerate(languages):
    print(f"{index}: {language}")

Output:

0: Python
1: JavaScript
2: Java
3: Go
4: Rust

Custom Starting Index

for index, language in enumerate(languages, start=1):
    print(f"{index}. {language}")

Output:

1. Python
2. JavaScript
3. Java
4. Go
5. Rust

enumerate() vs manual index counter:

# Without enumerate — verbose and error-prone
i = 0
for language in languages:
    print(f"{i}: {language}")
    i += 1

# With enumerate — clean and Pythonic
for i, language in enumerate(languages):
    print(f"{i}: {language}")

Always use enumerate() when you need both position and value. Creating a manual counter variable is unnecessary in Python.

Real use case — finding positions of matching items:

scores = [88, 92, 71, 95, 68, 88, 91]
target = 88

positions = [i for i, score in enumerate(scores) if score == target]
print(f"Score {target} found at positions: {positions}")
# Score 88 found at positions: [0, 5]

zip() — Iterating Multiple Sequences Together

zip() pairs up items from two or more sequences and yields them together:

names = ["Alice", "Bob", "Carol"]
scores = [88, 92, 79]
grades = ["B", "A", "C"]

for name, score, grade in zip(names, scores, grades):
    print(f"{name}: {score} ({grade})")

Output:

Alice: 88 (B)
Bob: 92 (A)
Carol: 79 (C)

zip() stops when the shortest sequence is exhausted. If sequences have different lengths, extra items from longer sequences are ignored:

a = [1, 2, 3, 4, 5]
b = ["a", "b", "c"]

for x, y in zip(a, b):
    print(x, y)
# 1 a
# 2 b
# 3 c
# 4 and 5 from a are ignored

Building Dictionaries With zip()

keys = ["name", "age", "city"]
values = ["Priya", 25, "Delhi"]

profile = dict(zip(keys, values))
print(profile)    # {'name': 'Priya', 'age': 25, 'city': 'Delhi'}

Iterating Over Dictionaries

Three patterns for looping over dictionaries — all covered in Day 7, reinforced here with loop context:

student = {"name": "Arjun", "age": 21, "gpa": 3.8, "city": "Mumbai"}

# Keys only (default)
for key in student:
    print(key)

# Values only
for value in student.values():
    print(value)

# Keys and values together — most common
for key, value in student.items():
    print(f"{key}: {value}")

Real use case — processing a list of dictionaries:

students = [
    {"name": "Alice", "score": 88},
    {"name": "Bob",   "score": 72},
    {"name": "Carol", "score": 95},
]

for student in students:
    grade = "A" if student["score"] >= 90 else "B" if student["score"] >= 80 else "C"
    print(f"{student['name']}: {student['score']} → {grade}")

Output:

Alice: 88 → B
Bob: 72 → C
Carol: 95 → A

The while Loop — Repeat Until a Condition Fails

A while loop runs its body repeatedly as long as its condition evaluates to True. When the condition becomes False, the loop ends:

count = 1

while count <= 5:
    print(f"Count: {count}")
    count += 1

print("Loop finished")

Output:

Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
Loop finished

The condition is checked before each iteration. When count reaches 6, count <= 5 is False and the loop stops.

while vs for — When to Use Each

Use for when you know what you are iterating over — a list, a range, a sequence. Use while when you are repeating until something changes and you do not know in advance how many iterations that will take:

# for loop — iterating a known sequence
for item in shopping_cart:
    process(item)

# while loop — repeating until a condition changes
attempts = 0
while not connection_established and attempts < 3:
    try_connect()
    attempts += 1

Input Validation With while

One of the most common real-world uses of while loops:

while True:
    age_input = input("Enter your age: ").strip()

    if age_input.isdigit():
        age = int(age_input)
        if 0 < age < 150:
            break    # valid input — exit the loop
        else:
            print("Age must be between 1 and 149")
    else:
        print("Please enter a number")

print(f"Your age is {age}")

The while True combined with break on valid input is the standard Python pattern for "keep asking until the user gives a valid answer."

Countdown and Accumulation Patterns

# Countdown
countdown = 10
while countdown > 0:
    print(countdown)
    countdown -= 1
print("Launch!")

# Accumulate until threshold
total = 0
transactions = [120, 450, 80, 310, 95, 500, 45]
i = 0

while total < 1000 and i < len(transactions):
    total += transactions[i]
    print(f"Added {transactions[i]}, running total: {total}")
    i += 1

print(f"Stopped at total: {total}")

break — Exit the Loop Immediately

break terminates the loop entirely when executed — Python jumps to the first line after the loop:

numbers = [4, 7, 2, 9, 1, 5, 8, 3]

for number in numbers:
    if number == 9:
        print(f"Found 9 at index {numbers.index(9)}")
        break    # stop searching, we found it
    print(f"Checked {number}")

Output:

Checked 4
Checked 7
Checked 2
Found 9 at index 3

Search Pattern With break

def find_first_negative(numbers):
    for i, num in enumerate(numbers):
        if num < 0:
            return i, num    # return exits function, not just loop
    return None, None        # not found

data = [5, 12, 8, -3, 7, -1, 4]
index, value = find_first_negative(data)
if index is not None:
    print(f"First negative: {value} at index {index}")
# First negative: -3 at index 3

break in while Loops

secret = "python123"
attempts = 0
max_attempts = 3

while attempts < max_attempts:
    password = input("Enter password: ")
    attempts += 1

    if password == secret:
        print("Access granted")
        break
    else:
        remaining = max_attempts - attempts
        if remaining > 0:
            print(f"Wrong password. {remaining} attempts remaining")
else:
    print("Account locked — too many failed attempts")

continue — Skip to the Next Iteration

continue skips the rest of the current iteration and moves immediately to the next one. The loop does not end — it just skips this particular pass:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

for number in numbers:
    if number % 2 == 0:
        continue    # skip even numbers
    print(number)

Output:

1
3
5
7
9

Filtering Invalid Data With continue

raw_data = ["alice", "", "bob", "  ", "carol", None, "david"]

valid_names = []
for item in raw_data:
    if not item or not item.strip():
        continue    # skip empty and whitespace-only entries
    valid_names.append(item.strip().title())

print(valid_names)    # ['Alice', 'Bob', 'Carol', 'David']

continue vs Nested if

Both approaches below produce identical results. continue often produces flatter, more readable code:

# Using nested if
for score in scores:
    if score >= 60:
        print(f"Pass: {score}")

# Using continue — equivalent, slightly flatter
for score in scores:
    if score < 60:
        continue
    print(f"Pass: {score}")

The continue style is particularly valuable when you have multiple validation checks before the main logic — it keeps the happy path at the left edge rather than deeply indented.

pass — The Placeholder

pass does nothing. It is a syntactic placeholder for a block that Python requires but you have not written yet:

for item in items:
    pass    # TODO: implement processing later

while condition:
    pass    # TODO: fill in later

if error:
    pass    # deliberately ignoring this case for now

pass is most useful during development when you are sketching out structure before filling in logic. In production code, a pass with a comment is usually either a genuine intentional no-op or a reminder to finish something.

else on Loops — The Underused Feature

Python loops have an optional else clause that runs when the loop completes normally — meaning it was not terminated by a break. This is unusual syntax that Python has but most other languages do not:

numbers = [4, 7, 2, 8, 1, 5]
target = 9

for number in numbers:
    if number == target:
        print(f"Found {target}")
        break
else:
    print(f"{target} was not found in the list")

# Output: 9 was not found in the list

The else only runs if break was never triggered. If break fires, else is skipped:

numbers = [4, 7, 2, 9, 1, 5]
target = 9

for number in numbers:
    if number == target:
        print(f"Found {target}")
        break
else:
    print(f"{target} was not found")

# Output: Found 9 — else does NOT run

This pattern is cleaner than using a boolean flag variable to track whether something was found:

# Without else — requires a flag variable
found = False
for number in numbers:
    if number == target:
        found = True
        break

if not found:
    print("Not found")

# With else — no flag needed, intent is clear
for number in numbers:
    if number == target:
        break
else:
    print("Not found")

Nested Loops — Loops Inside Loops

A nested loop has an inner loop that runs completely for each iteration of the outer loop:

for i in range(1, 4):
    for j in range(1, 4):
        print(f"{i} x {j} = {i * j}")
    print()    # blank line after each row

Output:

1 x 1 = 1
1 x 2 = 2
1 x 3 = 3

2 x 1 = 2
2 x 2 = 4
2 x 3 = 6

3 x 1 = 3
3 x 2 = 6
3 x 3 = 9

For a 3x3 grid: outer loop runs 3 times, inner loop runs 3 times per outer iteration — 9 total iterations.

Iterating 2D Data

classroom = [
    ["Alice", 88, 92, 79],
    ["Bob",   72, 68, 81],
    ["Carol", 95, 98, 92],
]

for row in classroom:
    name = row[0]
    scores = row[1:]
    average = sum(scores) / len(scores)
    print(f"{name}: {scores} → avg {average:.1f}")

Output:

Alice: [88, 92, 79] → avg 86.3
Bob: [72, 68, 81] → avg 73.7
Carol: [95, 98, 92] → avg 95.0

break in Nested Loops

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
target = 5
found = False

for row_index, row in enumerate(matrix):
    for col_index, value in enumerate(row):
        if value == target:
            print(f"Found {target} at row {row_index}, col {col_index}")
            found = True
            break    # exits inner loop only
    if found:
        break        # exits outer loop

Output:

Found 5 at row 1, col 1

Common Loop Patterns

These patterns appear constantly in real Python code.

Accumulation — Building a Total

prices = [29.99, 14.50, 89.99, 5.75, 44.00]

total = 0
for price in prices:
    total += price

print(f"Total: £{total:.2f}")    # Total: £184.23

Transformation — Building a New List

names = ["alice smith", "bob jones", "carol white"]

formatted = []
for name in names:
    formatted.append(name.title())

# More Pythonic with list comprehension
formatted = [name.title() for name in names]
print(formatted)    # ['Alice Smith', 'Bob Jones', 'Carol White']

Filtering — Keeping Only Matching Items

scores = [88, 55, 92, 61, 79, 45, 96, 70]

passing = [score for score in scores if score >= 60]
failing = [score for score in scores if score < 60]

print(f"Passing: {passing}")
print(f"Failing: {failing}")

Searching — Finding the First Match

def find_first_above(values, threshold):
    for i, value in enumerate(values):
        if value > threshold:
            return i, value
    return None, None

readings = [12, 18, 24, 31, 19, 28, 35]
index, value = find_first_above(readings, 30)
print(f"First reading above 30: {value} at index {index}")
# First reading above 30: 31 at index 3

Flattening — Combining Nested Lists

nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

flat = []
for sublist in nested:
    for item in sublist:
        flat.append(item)

# With list comprehension
flat = [item for sublist in nested for item in sublist]
print(flat)    # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Frequency Counting

words = ["python", "java", "python", "go", "java", "python", "rust"]

frequency = {}
for word in words:
    frequency[word] = frequency.get(word, 0) + 1

for language, count in sorted(frequency.items(), key=lambda x: x[1], reverse=True):
    print(f"{language}: {count}")

Output:

python: 3
java: 2
go: 1
rust: 1

A Complete Working Program

Here is a student exam analyser that uses every loop concept from this chapter:

# Student exam analyser
exam_data = [
    {"name": "Priya Sharma",  "scores": [88, 92, 79, 95, 84]},
    {"name": "Arjun Mehta",   "scores": [72, 65, 81, 70, 68]},
    {"name": "Cleo Park",     "scores": [95, 98, 92, 97, 99]},
    {"name": "Ravi Kumar",    "scores": [55, 60, 48, 62, 58]},
    {"name": "Maya Torres",   "scores": [83, 87, 90, 85, 88]},
]

def analyse_student(student):
    scores = student["scores"]
    average = sum(scores) / len(scores)
    highest = max(scores)
    lowest = min(scores)

    if average >= 90: grade = "A"
    elif average >= 80: grade = "B"
    elif average >= 70: grade = "C"
    elif average >= 60: grade = "D"
    else: grade = "F"

    return average, highest, lowest, grade

# Process all students
results = []
for student in exam_data:
    avg, high, low, grade = analyse_student(student)
    results.append({
        "name": student["name"],
        "average": avg,
        "highest": high,
        "lowest": low,
        "grade": grade
    })

# Print results table
print(f"{'Name':<16} {'Avg':>6} {'High':>6} {'Low':>5} {'Grade':>6}")
print("-" * 45)
for r in results:
    print(f"{r['name']:<16} {r['average']:>6.1f} {r['highest']:>6} "
          f"{r['lowest']:>5} {r['grade']:>6}")

# Class statistics using loops
all_averages = [r["average"] for r in results]
class_avg = sum(all_averages) / len(all_averages)
top_student = max(results, key=lambda x: x["average"])
failing = [r for r in results if r["grade"] == "F"]

# Find students who improved (last score higher than first)
print(f"\n=== Class Summary ===")
print(f"Class average:  {class_avg:.1f}")
print(f"Top performer:  {top_student['name']} ({top_student['average']:.1f})")
print(f"Failing students: {len(failing)}")

# Check if any student is failing — loop with else
print(f"\nFailing student check:")
for student in exam_data:
    avg = sum(student["scores"]) / len(student["scores"])
    if avg < 60:
        print(f"  {student['name']} needs support (avg: {avg:.1f})")
        break
else:
    print("  No students are currently failing")

Output:

Name              Avg   High   Low  Grade
---------------------------------------------
Priya Sharma      87.6   95    79      B
Arjun Mehta       71.2   81    65      C
Cleo Park         96.2   99    92      A
Ravi Kumar        56.6   62    48      F
Maya Torres       86.6   90    83      B

=== Class Summary ===
Class average:  79.6
Top performer:  Cleo Park (96.2)
Failing students: 1

Failing student check:
  Ravi Kumar needs support (avg: 56.6)

5 Loop Mistakes Every Beginner Makes

Mistake 1: Infinite while loop — forgetting to update the condition

# Wrong — count never changes, loops forever
count = 1
while count <= 5:
    print(count)
    # forgot count += 1

# Correct
count = 1
while count <= 5:
    print(count)
    count += 1    # must update or condition never becomes False

If your program hangs without output, an infinite loop is the first thing to check. Press Ctrl+C to interrupt it.

Mistake 2: Modifying a list while iterating over it

numbers = [1, 2, 3, 4, 5, 6]

# Wrong — skips items unpredictably
for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)

print(numbers)    # [1, 3, 5] — looks right but only by coincidence

# Safe — iterate over a copy
for n in numbers[:]:
    if n % 2 == 0:
        numbers.remove(n)

# Cleanest — use a comprehension
numbers = [n for n in numbers if n % 2 != 0]

Mistake 3: Using range(len(list)) when enumerate() is cleaner

items = ["apple", "banana", "cherry"]

# Verbose — old style
for i in range(len(items)):
    print(f"{i}: {items[i]}")

# Pythonic — use enumerate
for i, item in enumerate(items):
    print(f"{i}: {item}")

range(len(list)) is not wrong — but enumerate() is the Python way. Interviewers and code reviewers notice the difference.

Mistake 4: Off-by-one errors with range()

# Print numbers 1 to 10
for i in range(10):        # wrong — prints 0 to 9
    print(i)

for i in range(1, 10):     # wrong — prints 1 to 9, misses 10
    print(i)

for i in range(1, 11):     # correct — prints 1 to 10
    print(i)

Remember: range(start, stop) includes start but excludes stop. To include 10, stop must be 11.

Mistake 5: Expecting break to exit all nested loops

# break only exits the innermost loop
for i in range(3):
    for j in range(3):
        if j == 1:
            break    # exits inner loop only
    print(f"i={i} still runs")    # outer loop continues

# To exit both loops — use a flag or restructure into a function
def find_in_matrix(matrix, target):
    for i, row in enumerate(matrix):
        for j, val in enumerate(row):
            if val == target:
                return i, j    # return exits the function entirely
    return None, None

Practice: Loop Exercises

Exercise 1: for loop fundamentals

Write a program using a for loop and range() that prints the multiplication table for any number between 1 and 10. Format it cleanly: 7 x 1 = 7, 7 x 2 = 14, etc.

Exercise 2: while loop with validation

Write a number guessing game. Generate a secret number between 1 and 100. Use a while loop to keep asking the user to guess. Print "Too high", "Too low", or "Correct!" on each attempt. Count and display the number of attempts at the end.

Exercise 3: enumerate() and zip()

Given two lists — student names and their scores — use zip() to pair them, then enumerate() to add a ranking number. Print a ranked leaderboard sorted by score descending.

Exercise 4: Loop patterns

Given transactions = [250, -80, 500, -120, 300, -50, 1000, -200] (positive = deposit, negative = withdrawal), use a loop to calculate the final balance, total deposited, total withdrawn, and the number of each transaction type.

Exercise 5: Nested loops — pattern printing

Using nested loops, print the following pattern for n=5:

*
* *
* * *
* * * *
* * * * *

Exercise 6: Word frequency

Given a paragraph of text, write a program that counts how many times each word appears (case-insensitive, ignoring punctuation). Print the top 5 most frequent words.

See all Python Loop exercises with solutions

For all topics, see Python exercises.

What Comes Next — Day 10: Functions

You can now write Python programs that store data, make decisions, and repeat operations automatically. The next step is organisation. Functions let you give a name to a block of code and call it whenever you need it — once defined, a function runs anywhere you call it, with any input you give it.

Day 10 covers:

  • Defining functions with def
  • Parameters and return values
  • Default arguments and keyword arguments
  • *args and **kwargs for flexible functions
  • Variable scope — local vs global
  • Why well-designed functions are the foundation of maintainable code

Continue to Day 10: Functions

Chapter navigation

Frequently asked questions: Loops and iteration

What is the difference between a for loop and a while loop in Python?

A for loop iterates over a sequence — a list, string, range, or any iterable — and runs once for each item. Use it when you know what you are iterating over. A while loop repeats as long as a condition is True — use it when you are repeating until something changes and you do not know in advance how many iterations that requires.

What does range() do in Python?

range() generates a sequence of integers lazily — without creating them all in memory. range(n) generates 0 to n-1. range(start, stop) generates from start to stop-1. range(start, stop, step) generates with a custom step size, including negative steps for counting backwards. Convert to a list with list(range(...)) when you need a list.

What is enumerate() and why should I use it?

enumerate() wraps any iterable and yields both the index and the value at each step: for i, item in enumerate(items). Use it whenever you need both the position and the value in a loop. It replaces the verbose pattern of creating a counter variable manually and incrementing it each iteration.

What is the difference between break and continue in Python loops?

break exits the loop entirely — execution jumps to the first line after the loop. continue skips only the current iteration — execution jumps to the top of the loop for the next iteration. break is used to stop searching once a match is found. continue is used to skip items that do not meet a condition while processing the rest.

How do I avoid infinite loops in Python?

An infinite loop occurs when a while loop condition never becomes False. Always ensure the loop body contains code that eventually makes the condition false — typically by updating a counter (count += 1), modifying the variable being tested, or using break when a termination condition is met. If a program hangs, press Ctrl+C to interrupt it.

What does the else clause on a Python loop do?

A loop else clause runs when the loop completes normally — meaning it was not terminated by break. It does not run if break fired. This is most useful in search patterns: put the not-found logic in the else block and it only runs if the entire sequence was searched without finding a match.

What is zip() in Python and when should I use it?

zip() pairs up items from two or more sequences and yields them together as tuples. Use it when you have related data in separate lists and need to process them together. zip() stops at the shortest sequence. The dict(zip(keys, values)) pattern is a clean way to build dictionaries from two parallel lists.

How do I exit all levels of nested loops in Python?

break only exits the innermost loop. To exit all levels, the cleanest approach is to move the nested loops into a function and use return — which exits the function entirely. Alternatively, use a boolean flag variable that the outer loop checks.