Chapter 9 of 14

Loops

Master for loops, while loops, range(), enumerate(), zip(), break, continue, nested loops, and efficient iteration patterns.

Meritshot23 min read
PythonLoopsForWhileIteration
All Python Chapters

Why Loops?

Imagine you need to print a greeting for 1,000 students. Writing print() a thousand times is absurd. Loops solve this by letting you execute a block of code repeatedly — essential for processing collections, automating tasks, and building algorithms.

Loops are the backbone of the DRY principle (Don't Repeat Yourself). Instead of duplicating code, you write the logic once and let the loop handle repetition.

# Without a loop — repetitive and fragile
print("Hello, Student 1")
print("Hello, Student 2")
print("Hello, Student 3")
# ... imagine doing this 1,000 times

# With a loop — clean and scalable
for i in range(1, 1001):
    print(f"Hello, Student {i}")

Python provides two main loop constructs:

  • for loop — iterate over a sequence (list, string, range, dict, etc.)
  • while loop — repeat as long as a condition is True

for Loop

The for loop iterates over any iterable — an object that can return its items one at a time. Lists, strings, tuples, dictionaries, sets, files, and ranges are all iterables.

Iterating Over a List

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

for fruit in fruits:
    print(fruit)

Output:

apple
banana
cherry

The variable fruit takes the value of each element in turn. You can name it anything meaningful.

Iterating Over a String

A string is a sequence of characters, so for walks through each character:

for char in "Python":
    print(char, end=" ")
# P y t h o n

Iterating Over a Tuple

coordinates = (10.5, 20.3, 30.1)

for coord in coordinates:
    print(coord)

Iterating Over a Dictionary

By default, iterating over a dict gives you the keys:

student = {"name": "Priya", "age": 22, "grade": "A"}

# Keys
for key in student:
    print(key)
# name, age, grade

# Values
for value in student.values():
    print(value)
# Priya, 22, A

# Key-value pairs
for key, value in student.items():
    print(f"{key}: {value}")
# name: Priya
# age: 22
# grade: A

Iterating Over a Set

Sets are unordered, so the iteration order is not guaranteed:

unique_colors = {"red", "green", "blue", "red"}

for color in unique_colors:
    print(color)
# Order may vary: blue, red, green

range() Function — In Depth

range() generates a sequence of integers on demand. It does not create a list in memory — it is a lazy, memory-efficient object that produces numbers one at a time.

range(stop)

Generates integers from 0 up to (but not including) stop:

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

range(start, stop)

Generates integers from start up to (but not including) stop:

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

range(start, stop, step)

Generates integers from start, incrementing by step, stopping before stop:

# Even numbers from 0 to 10
for i in range(0, 11, 2):
    print(i)
# 0, 2, 4, 6, 8, 10

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

Negative Step — Counting Backwards

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

# Countdown
for i in range(5, 0, -1):
    print(f"{i}...")
print("Liftoff!")

Memory Efficiency (Lazy Evaluation)

range() does not store all numbers in memory. It calculates each number on the fly:

import sys

# A range of 1 million numbers uses very little memory
r = range(1_000_000)
print(sys.getsizeof(r))  # ~48 bytes — the same regardless of size

# Contrast with a list of 1 million numbers
nums = list(range(1_000_000))
print(sys.getsizeof(nums))  # ~8 MB

This makes range() ideal for very large iterations. You can even write range(10**18) without running out of memory — it never stores the full sequence.

Useful range() Tricks

# Check membership efficiently (O(1) thanks to lazy evaluation)
print(500 in range(1000))  # True
print(1001 in range(1000)) # False

# Get length
print(len(range(0, 100, 3)))  # 34

# Convert to list when you need actual values
print(list(range(5)))  # [0, 1, 2, 3, 4]

enumerate() — Loop with Index

When you need both the index and the value, use enumerate() instead of manually tracking a counter variable.

languages = ["Python", "SQL", "Tableau", "Power BI"]

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

Output:

0: Python
1: SQL
2: Tableau
3: Power BI

Custom Start Index

By default enumerate starts at 0. Pass a second argument to change it:

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

Output:

1. Python
2. SQL
3. Tableau
4. Power BI

Why Not Use range(len(...))?

# Avoid this — less readable, more error-prone
for i in range(len(languages)):
    print(f"{i}: {languages[i]}")

# Prefer this — Pythonic and clean
for i, lang in enumerate(languages):
    print(f"{i}: {lang}")

The enumerate version is easier to read, less likely to cause index errors, and considered the Pythonic way.


zip() — Parallel Iteration

zip() takes two or more iterables and pairs their elements together, yielding tuples:

names = ["Priya", "Rahul", "Ananya"]
scores = [95, 82, 90]
grades = ["A", "B", "A"]

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

Output:

Priya: 95 (A)
Rahul: 82 (B)
Ananya: 90 (A)

Unequal Lengths

zip() stops at the shortest iterable:

a = [1, 2, 3, 4, 5]
b = ["x", "y", "z"]

for num, letter in zip(a, b):
    print(num, letter)
# 1 x
# 2 y
# 3 z
# Items 4 and 5 are silently dropped

zip_longest — Include All Items

Use itertools.zip_longest to continue until the longest iterable is exhausted, filling missing values with a default:

from itertools import zip_longest

a = [1, 2, 3, 4, 5]
b = ["x", "y", "z"]

for num, letter in zip_longest(a, b, fillvalue="-"):
    print(num, letter)
# 1 x
# 2 y
# 3 z
# 4 -
# 5 -

Unzipping with zip(*)

You can "unzip" a list of tuples back into separate lists:

pairs = [("Priya", 95), ("Rahul", 82), ("Ananya", 90)]
names, scores = zip(*pairs)

print(names)   # ('Priya', 'Rahul', 'Ananya')
print(scores)  # (95, 82, 90)

Building a Dictionary with zip

keys = ["name", "age", "city"]
values = ["Rahul", 25, "Mumbai"]

info = dict(zip(keys, values))
print(info)  # {'name': 'Rahul', 'age': 25, 'city': 'Mumbai'}

while Loop

The while loop repeats a block as long as a condition evaluates to True. Use it when you do not know in advance how many iterations are needed.

Basic while Loop

count = 0

while count < 5:
    print(count)
    count += 1
# 0, 1, 2, 3, 4

Warning: Always make sure the condition eventually becomes False. Otherwise, you create an infinite loop.

Input Validation

A classic use case — keep asking until the user provides valid input:

while True:
    age = input("Enter your age: ")
    if age.isdigit() and 0 < int(age) < 150:
        age = int(age)
        break
    print("Invalid age. Please enter a number between 1 and 149.")

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

Sentinel Value Pattern

Loop until a special "sentinel" value signals termination:

total = 0
print("Enter numbers to sum. Type -1 to stop.")

while True:
    num = int(input("Number: "))
    if num == -1:
        break
    total += num

print(f"Total: {total}")

Infinite Loops (Intentional)

Some programs intentionally run forever — servers, game loops, monitoring scripts:

# A simple event loop (conceptual)
while True:
    event = get_next_event()  # hypothetical function
    process(event)
    if should_shutdown():
        break

while with else

Just like for, a while loop can have an else clause that runs when the condition becomes False naturally (not via break):

attempts = 0
max_attempts = 3

while attempts < max_attempts:
    password = input("Enter password: ")
    if password == "secret123":
        print("Access granted!")
        break
    attempts += 1
else:
    print("Account locked after 3 failed attempts.")

break, continue, and pass

break — Exit the Loop Immediately

break terminates the innermost loop entirely:

# Find the first negative number
numbers = [10, 25, -3, 40, -7, 60]

for num in numbers:
    if num < 0:
        print(f"First negative number: {num}")
        break
# First negative number: -3

continue — Skip to Next Iteration

continue skips the rest of the current iteration and jumps to the next one:

# Print only odd numbers
for num in range(1, 11):
    if num % 2 == 0:
        continue
    print(num)
# 1, 3, 5, 7, 9

pass — Do Nothing (Placeholder)

pass is a no-op. Use it when syntax requires a statement but you have nothing to execute yet:

for i in range(10):
    pass  # TODO: implement later

# Useful in empty function/class definitions
def process_data():
    pass  # placeholder

Combining break and continue

# Process only valid items, stop at a poison pill
tasks = ["email", "report", "", "STOP", "backup"]

for task in tasks:
    if task == "STOP":
        print("Stopping task processor.")
        break
    if not task:
        continue  # skip empty tasks
    print(f"Processing: {task}")

# Processing: email
# Processing: report
# Stopping task processor.

else Clause on Loops — A Unique Python Feature

Both for and while loops can have an else block. It executes only if the loop completes normally — meaning it was not terminated by break.

Practical Use Case: Search with Fallback

# Check if a number is prime
def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False  # found a divisor, not prime
    return True  # loop completed, no divisor found

The for-else version makes the intent even clearer:

def check_prime(n):
    if n < 2:
        print(f"{n} is not prime")
        return
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            print(f"{n} is not prime (divisible by {i})")
            break
    else:
        print(f"{n} is prime")

check_prime(17)  # 17 is prime
check_prime(24)  # 24 is not prime (divisible by 2)

Search in a Collection

students = ["Priya", "Rahul", "Ananya", "Vikram"]
target = "Ananya"

for student in students:
    if student == target:
        print(f"Found {target}!")
        break
else:
    print(f"{target} not found in the list.")

Think of else as "no break" — if the loop runs to completion without hitting break, the else block executes.


Nested Loops

A loop inside another loop. The inner loop runs completely for each iteration of the outer loop.

Multiplication Table

for i in range(1, 6):
    for j in range(1, 11):
        print(f"{i * j:4}", end="")
    print()

Output:

   1   2   3   4   5   6   7   8   9  10
   2   4   6   8  10  12  14  16  18  20
   3   6   9  12  15  18  21  24  27  30
   4   8  12  16  20  24  28  32  36  40
   5  10  15  20  25  30  35  40  45  50

Pattern Printing — Right Triangle

rows = 5

for i in range(1, rows + 1):
    print("* " * i)

Output:

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

Pattern Printing — Pyramid

rows = 5

for i in range(1, rows + 1):
    spaces = " " * (rows - i)
    stars = "* " * i
    print(spaces + stars)

Output:

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

Pattern Printing — Diamond

rows = 5

# Upper half (including the middle row)
for i in range(1, rows + 1):
    print(" " * (rows - i) + "* " * i)

# Lower half
for i in range(rows - 1, 0, -1):
    print(" " * (rows - i) + "* " * i)

Output:

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

Iterating Over 2D Structures

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

for row in matrix:
    for element in row:
        print(f"{element:3}", end="")
    print()

Breaking Out of Nested Loops

break only exits the innermost loop. To break out of multiple levels, use a flag or move the logic into a function:

# Using a flag
found = False
for row in matrix:
    for element in row:
        if element == 5:
            print(f"Found 5!")
            found = True
            break
    if found:
        break

# Cleaner: use a function
def find_element(matrix, target):
    for i, row in enumerate(matrix):
        for j, element in enumerate(row):
            if element == target:
                return (i, j)
    return None

position = find_element(matrix, 5)
print(position)  # (1, 1)

Loop Control Patterns

These are common algorithmic patterns you will use repeatedly.

Accumulator (Running Total)

# Sum all values
numbers = [10, 20, 30, 40, 50]
total = 0

for num in numbers:
    total += num

print(f"Sum: {total}")  # Sum: 150

Counter

# Count vowels in a string
text = "Hello, World!"
vowel_count = 0

for char in text.lower():
    if char in "aeiou":
        vowel_count += 1

print(f"Vowels: {vowel_count}")  # Vowels: 3

Flag (Boolean Sentinel)

# Check if any item meets a condition
numbers = [2, 4, 7, 8, 10]
has_odd = False

for num in numbers:
    if num % 2 != 0:
        has_odd = True
        break

print(f"Contains odd number: {has_odd}")  # True

# Python shortcut:
has_odd = any(num % 2 != 0 for num in numbers)

Search (Find First Match)

# Find the first word longer than 5 characters
words = ["cat", "elephant", "dog", "butterfly", "ant"]
result = None

for word in words:
    if len(word) > 5:
        result = word
        break

print(f"First long word: {result}")  # elephant

# Python shortcut:
result = next((w for w in words if len(w) > 5), None)

Running Max / Min

temperatures = [22.5, 25.1, 19.8, 30.2, 28.7, 15.3]

running_max = temperatures[0]
running_min = temperatures[0]

for temp in temperatures[1:]:
    if temp > running_max:
        running_max = temp
    if temp < running_min:
        running_min = temp

print(f"Max: {running_max}, Min: {running_min}")
# Max: 30.2, Min: 15.3

Common Iteration Patterns

Filtering

Select items that match a condition:

scores = [45, 82, 67, 91, 54, 78, 95, 60]

# Loop approach
passing = []
for score in scores:
    if score >= 60:
        passing.append(score)

# List comprehension (preferred for simple filters)
passing = [s for s in scores if s >= 60]

# Built-in filter()
passing = list(filter(lambda s: s >= 60, scores))

print(passing)  # [82, 67, 91, 78, 95, 60]

Mapping (Transforming)

Apply a transformation to each item:

names = ["priya", "rahul", "ananya"]

# Loop approach
capitalized = []
for name in names:
    capitalized.append(name.capitalize())

# List comprehension (preferred)
capitalized = [name.capitalize() for name in names]

# Built-in map()
capitalized = list(map(str.capitalize, names))

print(capitalized)  # ['Priya', 'Rahul', 'Ananya']

Reducing (Aggregating)

Combine all items into a single result:

from functools import reduce

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

# Loop approach
product = 1
for num in numbers:
    product *= num

# Using reduce
product = reduce(lambda a, b: a * b, numbers)

print(product)  # 120

Grouping

Organize items into categories:

words = ["apple", "banana", "avocado", "blueberry", "cherry", "apricot"]

groups = {}
for word in words:
    first_letter = word[0]
    if first_letter not in groups:
        groups[first_letter] = []
    groups[first_letter].append(word)

print(groups)
# {'a': ['apple', 'avocado', 'apricot'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}

# Using setdefault (cleaner)
groups = {}
for word in words:
    groups.setdefault(word[0], []).append(word)

# Using collections.defaultdict (cleanest)
from collections import defaultdict
groups = defaultdict(list)
for word in words:
    groups[word[0]].append(word)

Sliding Window

Process overlapping subsequences:

# Moving average with window size 3
data = [10, 20, 30, 40, 50, 60, 70]
window_size = 3

averages = []
for i in range(len(data) - window_size + 1):
    window = data[i:i + window_size]
    averages.append(sum(window) / window_size)

print(averages)  # [20.0, 30.0, 40.0, 50.0, 60.0]

Performance: Comprehensions, Generators, and itertools

List Comprehension vs Loop

List comprehensions are typically faster than equivalent for loops with .append() because they are optimized internally:

import time

# Loop approach
start = time.time()
squares_loop = []
for i in range(1_000_000):
    squares_loop.append(i ** 2)
loop_time = time.time() - start

# Comprehension approach
start = time.time()
squares_comp = [i ** 2 for i in range(1_000_000)]
comp_time = time.time() - start

print(f"Loop: {loop_time:.4f}s")
print(f"Comprehension: {comp_time:.4f}s")
# Comprehensions are typically 20-30% faster

Generator Expressions for Memory Efficiency

When you only need to iterate once and the data is large, use a generator expression instead of a list comprehension. It produces items one at a time, saving memory:

# List comprehension — stores ALL squares in memory
squares_list = [i ** 2 for i in range(10_000_000)]  # ~80 MB

# Generator expression — stores almost nothing
squares_gen = (i ** 2 for i in range(10_000_000))    # ~100 bytes

# You can iterate over it the same way
total = sum(i ** 2 for i in range(10_000_000))  # no extra memory

itertools Highlights

The itertools module provides high-performance building blocks for iteration:

from itertools import chain, product, combinations, permutations, islice, groupby

# chain — concatenate multiple iterables
for item in chain([1, 2], [3, 4], [5]):
    print(item, end=" ")
# 1 2 3 4 5

# product — Cartesian product (replaces nested loops)
for color, size in product(["red", "blue"], ["S", "M", "L"]):
    print(f"{color}-{size}", end=" ")
# red-S red-M red-L blue-S blue-M blue-L

# combinations — all r-length combinations (no repeats)
for combo in combinations([1, 2, 3, 4], 2):
    print(combo, end=" ")
# (1,2) (1,3) (1,4) (2,3) (2,4) (3,4)

# permutations — all r-length permutations (order matters)
for perm in permutations("ABC", 2):
    print("".join(perm), end=" ")
# AB AC BA BC CA CB

# islice — slice any iterable lazily (no indexing needed)
from itertools import count  # infinite counter
for num in islice(count(100), 5):
    print(num, end=" ")
# 100 101 102 103 104

# groupby — group consecutive items by key
data = sorted(["apple", "avocado", "banana", "blueberry", "cherry"], key=lambda x: x[0])
for key, group in groupby(data, key=lambda x: x[0]):
    print(f"{key}: {list(group)}")
# a: ['apple', 'avocado']
# b: ['banana', 'blueberry']
# c: ['cherry']

Avoiding Common Mistakes

Mistake 1: Modifying a List While Iterating

This leads to skipped items or errors:

# WRONG — modifying list during iteration
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)  # Dangerous!
print(numbers)  # [1, 3, 5, 6] — 6 was SKIPPED!

# CORRECT — 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]

# CORRECT — iterate over a copy using slicing
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers[:]:  # numbers[:] creates a copy
    if num % 2 == 0:
        numbers.remove(num)
print(numbers)  # [1, 3, 5]

Mistake 2: Infinite Loops

Forgetting to update the loop variable:

# WRONG — infinite loop!
count = 0
while count < 5:
    print(count)
    # Forgot: count += 1

# CORRECT
count = 0
while count < 5:
    print(count)
    count += 1

Mistake 3: Off-by-One Errors

Misunderstanding range() boundaries:

# Want to print 1 to 10?
for i in range(10):       # WRONG: prints 0-9
    print(i)

for i in range(1, 10):    # WRONG: prints 1-9
    print(i)

for i in range(1, 11):    # CORRECT: prints 1-10
    print(i)

Mistake 4: Using Index When You Do Not Need It

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

# Avoid — unnecessary index variable
for i in range(len(colors)):
    print(colors[i])

# Prefer — direct iteration
for color in colors:
    print(color)

Practical Examples

1. Number Guessing Game

import random

secret = random.randint(1, 100)
attempts = 0

print("Guess the number between 1 and 100!")

while True:
    guess = int(input("Your guess: "))
    attempts += 1

    if guess < secret:
        print("Too low!")
    elif guess > secret:
        print("Too high!")
    else:
        print(f"Correct! You got it in {attempts} attempts.")
        break

2. Prime Number Finder — Sieve of Eratosthenes

The most efficient way to find all primes up to a given limit:

def sieve_of_eratosthenes(limit):
    """Find all prime numbers up to 'limit' using the Sieve of Eratosthenes."""
    is_prime = [True] * (limit + 1)
    is_prime[0] = is_prime[1] = False

    for num in range(2, int(limit**0.5) + 1):
        if is_prime[num]:
            # Mark all multiples of num as not prime
            for multiple in range(num * num, limit + 1, num):
                is_prime[multiple] = False

    primes = [num for num, prime in enumerate(is_prime) if prime]
    return primes

print(sieve_of_eratosthenes(50))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

3. FizzBuzz

A classic programming exercise:

for num in range(1, 101):
    if num % 15 == 0:
        print("FizzBuzz")
    elif num % 3 == 0:
        print("Fizz")
    elif num % 5 == 0:
        print("Buzz")
    else:
        print(num)

Tip: Check % 15 first (or equivalently % 3 == 0 and % 5 == 0) because a number divisible by both 3 and 5 must print "FizzBuzz", not just "Fizz" or "Buzz".

4. Fibonacci Sequence

def fibonacci(n):
    """Generate the first n Fibonacci numbers."""
    if n <= 0:
        return []
    if n == 1:
        return [0]

    fib = [0, 1]
    for i in range(2, n):
        fib.append(fib[-1] + fib[-2])
    return fib

print(fibonacci(10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Generator version (memory-efficient for large sequences)
def fib_generator():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

from itertools import islice
print(list(islice(fib_generator(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

5. Pattern Printer — Comprehensive

def print_pattern(pattern_type, rows=5):
    """Print various star patterns."""
    if pattern_type == "right_triangle":
        for i in range(1, rows + 1):
            print("* " * i)

    elif pattern_type == "inverted_triangle":
        for i in range(rows, 0, -1):
            print("* " * i)

    elif pattern_type == "pyramid":
        for i in range(1, rows + 1):
            print(" " * (rows - i) + "* " * i)

    elif pattern_type == "diamond":
        for i in range(1, rows + 1):
            print(" " * (rows - i) + "* " * i)
        for i in range(rows - 1, 0, -1):
            print(" " * (rows - i) + "* " * i)

    elif pattern_type == "hollow_square":
        for i in range(rows):
            for j in range(rows):
                if i == 0 or i == rows - 1 or j == 0 or j == rows - 1:
                    print("* ", end="")
                else:
                    print("  ", end="")
            print()

# Try each one:
for pattern in ["right_triangle", "pyramid", "diamond", "hollow_square"]:
    print(f"\n--- {pattern} ---")
    print_pattern(pattern, 5)

6. Word Frequency Counter

text = """Python is a great language. Python is easy to learn.
Many developers love Python because Python is versatile."""

# Clean and split
words = text.lower().replace(".", "").split()

# Count frequencies
freq = {}
for word in words:
    freq[word] = freq.get(word, 0) + 1

# Sort by frequency (descending)
sorted_freq = sorted(freq.items(), key=lambda x: x[1], reverse=True)

print("Word Frequencies:")
for word, count in sorted_freq:
    bar = "#" * count
    print(f"  {word:12} {count} {bar}")

Practice Exercises

Exercise 1: Sum of Digits

Write a program that takes a positive integer and computes the sum of its digits using a loop.

Input: 12345
Output: 15  (1 + 2 + 3 + 4 + 5)

Hint: Convert to string and iterate over characters, or use modulo % 10 and integer division // 10.

Exercise 2: Reverse a String

Write a program that reverses a string using a for loop (do not use slicing or reversed()).

Input: "Python"
Output: "nohtyP"

Exercise 3: Collatz Sequence

Given a positive integer n, generate the Collatz sequence:

  • If n is even, the next number is n // 2
  • If n is odd, the next number is 3 * n + 1
  • Stop when n reaches 1
Input: 6
Output: 6 → 3 → 10 → 5 → 16 → 8 → 4 → 2 → 1

Hint: Use a while loop that continues until n == 1.

Exercise 4: Flatten a 2D List

Write a function that takes a list of lists and returns a single flat list.

Input: [[1, 2], [3, 4, 5], [6]]
Output: [1, 2, 3, 4, 5, 6]

Hint: Use nested for loops or a list comprehension with two for clauses.

Exercise 5: Password Validator

Write a program that keeps asking for a password until it meets ALL of these criteria:

  • At least 8 characters long
  • Contains at least one uppercase letter
  • Contains at least one digit
  • Contains at least one special character (!@#$%^&*)

Hint: Use a while loop with break, and check each condition using any() and character method loops.

Exercise 6: Matrix Transposition

Write a function that transposes a matrix (swap rows and columns) using nested loops.

Input:                Output:
[[1, 2, 3],          [[1, 4],
 [4, 5, 6]]           [2, 5],
                       [3, 6]]

Hint: The element at position [i][j] in the original becomes [j][i] in the transposed matrix. Use zip(*matrix) for a one-liner alternative.


Summary

In this chapter, you learned:

  • for loops for iterating over sequences — lists, strings, tuples, dicts, sets, and ranges
  • range() in depth — start, stop, step, negative ranges, and its memory efficiency via lazy evaluation
  • enumerate() for getting both index and value without manual counter tracking
  • zip() for parallel iteration and zip_longest for unequal-length iterables
  • while loops for condition-based repetition — input validation, sentinel values, and infinite loops
  • break, continue, and pass for fine-grained loop control
  • The else clause on loops — a unique Python feature that runs when no break occurs
  • Nested loops for tables, patterns, and 2D data structures
  • Loop control patterns — accumulator, counter, flag, search, running max/min
  • Iteration patterns — filtering, mapping, reducing, grouping, and sliding windows
  • Performance — list comprehensions vs loops, generator expressions, and itertools for high-performance iteration
  • Common mistakes — modifying lists during iteration, infinite loops, off-by-one errors

Loops are one of the most powerful constructs in programming. Combined with conditionals from the previous chapter, you now have the tools to build algorithms that can process, transform, and analyze any data.