Python Loops: for and while

Week 2 Day 2: Control Flow Fundamentals

Introduction to Loops

Welcome to our exploration of loops in Python! If conditional statements are the decision-makers in programming, loops are the workhorses—they allow us to execute code repeatedly, automating repetitive tasks that would be impractical to write manually.

Imagine you're planting a garden. If conditional statements are like deciding which plants go where based on sun exposure, loops are like the act of planting 100 seeds one by one. You wouldn't write out 100 separate planting instructions—you'd use a loop to repeat the planting process.

The code for this lesson can be found in the /week2/day2/python_loops.py file in your course repository.

Types of Loops in Python

Python provides two primary loop structures:

Think of a for loop as a tour guide taking you to each specific landmark on a predefined route, while a while loop is more like a security guard who keeps patrolling until something changes.

For Loops: Iterating Over Sequences

The for loop in Python is designed for iterating over a sequence. Unlike for loops in some other programming languages that focus on numeric ranges, Python's for loop is more like a "for each" loop.

Basic Syntax

for item in sequence:
    # Code to execute for each item
    statement1
    statement2
    # ... more statements

In this pattern:

Example: Iterating Over a List

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

for fruit in fruits:
    print(f"I like {fruit}s.")

# Output:
# I like apples.
# I like bananas.
# I like cherrys.
# I like dates.

Notice how the variable fruit takes on each value in the list, one at a time, and the print statement executes for each value.

Loops with Strings

message = "Python"

for character in message:
    print(character)

# Output:
# P
# y
# t
# h
# o
# n

Strings in Python are sequences of characters, so a for loop iterates through each character.

Loops with Dictionaries

student = {
    "name": "Alice",
    "age": 25,
    "courses": ["Python", "Data Science", "Web Development"]
}

# Iterating over keys (default)
for key in student:
    print(key)

# Output:
# name
# age
# courses

# Iterating over values
for value in student.values():
    print(value)

# Output:
# Alice
# 25
# ['Python', 'Data Science', 'Web Development']

# Iterating over key-value pairs
for key, value in student.items():
    print(f"{key}: {value}")

# Output:
# name: Alice
# age: 25
# courses: ['Python', 'Data Science', 'Web Development']

When iterating over dictionaries, you can choose to access keys, values, or both together.

Using the range() Function

The range() function generates a sequence of numbers, which is commonly used with for loops when you need to perform an action a specific number of times.

# range(stop) - Generates numbers from 0 to stop-1
for i in range(5):
    print(i)

# Output:
# 0
# 1
# 2
# 3
# 4

# range(start, stop) - Generates numbers from start to stop-1
for i in range(2, 7):
    print(i)

# Output:
# 2
# 3
# 4
# 5
# 6

# range(start, stop, step) - Generates numbers from start to stop-1 with a step interval
for i in range(1, 10, 2):
    print(i)

# Output:
# 1
# 3
# 5
# 7
# 9

Think of range() as a tour planner that creates an itinerary of stops (numbers) for your for loop to visit.

Accessing Index with enumerate()

Sometimes you need both the index position and the value of each item in a sequence. The enumerate() function makes this easy.

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

for index, fruit in enumerate(fruits):
    print(f"Index {index}: {fruit}")

# Output:
# Index 0: apple
# Index 1: banana
# Index 2: cherry
# Index 3: date

# You can also specify a starting index
for index, fruit in enumerate(fruits, start=1):
    print(f"Fruit #{index}: {fruit}")

# Output:
# Fruit #1: apple
# Fruit #2: banana
# Fruit #3: cherry
# Fruit #4: date

This is like having a tour guide who both shows you each landmark and tells you which stop number you're at.

Real-World Example: Processing Data Records

students = [
    {"name": "Alice", "grade": 85, "attendance": 0.92},
    {"name": "Bob", "grade": 72, "attendance": 0.85},
    {"name": "Charlie", "grade": 90, "attendance": 0.95},
    {"name": "Diana", "grade": 65, "attendance": 0.70},
    {"name": "Eliza", "grade": 91, "attendance": 0.98}
]

# Calculate class average
total_grade = 0
high_performers = []

for student in students:
    total_grade += student["grade"]
    
    # Identify high performers (good grades and attendance)
    if student["grade"] >= 85 and student["attendance"] >= 0.9:
        high_performers.append(student["name"])

average_grade = total_grade / len(students)
print(f"Class average grade: {average_grade:.2f}")
print(f"High performers: {', '.join(high_performers)}")

# Output:
# Class average grade: 80.60
# High performers: Alice, Charlie, Eliza

This example shows how loops are essential for data processing tasks, allowing us to analyze multiple records with a single block of code.

While Loops: Repeating Based on Conditions

Unlike for loops that iterate over a sequence, while loops continue executing as long as a specified condition remains true. They're perfect for situations where you don't know in advance how many iterations you'll need.

Basic Syntax

while condition:
    # Code to execute while the condition is True
    statement1
    statement2
    # ... more statements

In this pattern:

Example: Basic While Loop

count = 1

while count <= 5:
    print(f"Count is: {count}")
    count += 1  # Without this, we'd have an infinite loop!

# Output:
# Count is: 1
# Count is: 2
# Count is: 3
# Count is: 4
# Count is: 5

Notice the crucial update to count within the loop. Without it, the condition would always be true, creating an infinite loop!

Using While for User Input Validation

valid_input = False

while not valid_input:
    age = input("Enter your age (18-100): ")
    
    if age.isdigit():
        age = int(age)
        if 18 <= age <= 100:
            valid_input = True
            print(f"Thank you, your age ({age}) has been recorded.")
        else:
            print("Age must be between 18 and 100.")
    else:
        print("Please enter a valid number.")

This loop continues asking for input until the user provides a valid age. It's a perfect example of not knowing in advance how many iterations will be needed.

While Loop with a Sentinel Value

print("Enter numbers to sum (enter 'done' to finish)")

sum_total = 0
user_input = ""

while user_input.lower() != 'done':
    user_input = input("Enter a number (or 'done'): ")
    
    if user_input.lower() == 'done':
        break  # Exit the loop early
    
    if user_input.isdigit():
        sum_total += int(user_input)
    else:
        print("That's not a valid number. Try again.")

print(f"The sum of your numbers is: {sum_total}")

This pattern uses a "sentinel value" ('done') to signal when to end the loop. It's commonly used when reading input until a specific termination signal.

Building a Simple Game Loop

import random

health = 100
rounds = 0
game_active = True

print("Welcome to Dragon Battle! Try to defeat the dragon.")

while game_active:
    rounds += 1
    
    print(f"\nRound {rounds}")
    print(f"Your health: {health}")
    
    action = input("What will you do? (attack/heal/run): ").lower()
    
    if action == "attack":
        player_damage = random.randint(10, 25)
        dragon_damage = random.randint(5, 20)
        
        print(f"You dealt {player_damage} damage to the dragon!")
        print(f"The dragon strikes back and deals {dragon_damage} damage!")
        
        health -= dragon_damage
        
        if player_damage >= 20:
            print("Critical hit! The dragon is severely wounded!")
            game_active = False
            print("Victory! You've defeated the dragon!")
    
    elif action == "heal":
        heal_amount = random.randint(10, 20)
        dragon_damage = random.randint(5, 10)
        
        health += heal_amount
        health -= dragon_damage
        
        print(f"You restored {heal_amount} health!")
        print(f"The dragon deals {dragon_damage} damage while you're healing.")
    
    elif action == "run":
        escape_chance = random.random()
        
        if escape_chance > 0.5:
            print("You escaped successfully!")
            game_active = False
        else:
            dragon_damage = random.randint(10, 25)
            health -= dragon_damage
            print(f"You failed to escape! The dragon deals {dragon_damage} damage!")
    
    else:
        print("Invalid action. You hesitate and lose your turn!")
        health -= random.randint(5, 10)
    
    # Check for defeat condition
    if health <= 0:
        print("Your health has reached zero. The dragon has defeated you!")
        game_active = False

print("\nGame Over!")

This example demonstrates a game loop, one of the most common applications of while loops in programming. The loop continues until a win or loss condition is met.

Loop Control: break, continue, pass

Python provides three statements that can alter the flow of a loop: break, continue, and pass.

The break Statement

The break statement immediately terminates the loop and transfers control to the statement following the loop.

# Finding the first even number in a list
numbers = [5, 7, 11, 2, 9, 8, 3]

for num in numbers:
    if num % 2 == 0:
        print(f"Found an even number: {num}")
        break  # Exit loop after finding first even number

# Output:
# Found an even number: 2

Think of break as an emergency exit—it allows you to leave the loop immediately when a specific condition is met.

The continue Statement

The continue statement skips the rest of the current iteration and jumps to the next iteration of the loop.

# Printing only odd numbers
for num in range(1, 10):
    if num % 2 == 0:
        continue  # Skip even numbers
    print(num)

# Output:
# 1
# 3
# 5
# 7
# 9

Think of continue as a "skip" button—it allows you to bypass certain iterations based on specific conditions.

The pass Statement

The pass statement is a null operation: it does nothing. It's used as a placeholder when a statement is syntactically required but you don't want any action.

# Using pass as a placeholder
for item in some_list:
    if condition1:
        # Do something
    elif condition2:
        # Do something else
    else:
        pass  # Nothing to do here, but syntactically needed

Think of pass as a "do nothing" instruction—it's useful for creating syntactically correct code blocks that don't have any implementation yet.

Practical Example: Data Filtering with Control Statements

def process_transactions(transactions):
    approved = []
    flagged = []
    
    for transaction in transactions:
        # Skip processing for zero-amount transactions
        if transaction["amount"] == 0:
            continue
        
        # Flag suspicious high-value transactions
        if transaction["amount"] > 10000:
            transaction["status"] = "flagged"
            flagged.append(transaction)
            continue
        
        # Check for blacklisted merchants
        if transaction["merchant"] in blacklisted_merchants:
            transaction["status"] = "rejected"
            continue
        
        # If all checks pass, approve the transaction
        transaction["status"] = "approved"
        approved.append(transaction)
    
    return approved, flagged

# Sample usage
transactions = [
    {"id": 1, "amount": 75.50, "merchant": "Grocery Store"},
    {"id": 2, "amount": 0, "merchant": "Test Transaction"},
    {"id": 3, "amount": 12500, "merchant": "Car Dealership"},
    {"id": 4, "amount": 199.99, "merchant": "Scam Website"},
    {"id": 5, "amount": 850, "merchant": "Online Retailer"}
]

blacklisted_merchants = ["Scam Website", "Fraudulent Store"]

approved, flagged = process_transactions(transactions)

print("Approved transactions:")
for t in approved:
    print(f"ID: {t['id']}, Amount: ${t['amount']}, Merchant: {t['merchant']}")

print("\nFlagged transactions:")
for t in flagged:
    print(f"ID: {t['id']}, Amount: ${t['amount']}, Merchant: {t['merchant']}")

This example demonstrates how continue can be used to implement sophisticated filtering logic in data processing applications.

Infinite Loops and How to Avoid Them

An infinite loop is a loop that continues indefinitely because its termination condition is never met. While sometimes intentional, infinite loops are often the result of programming errors.

Intentional Infinite Loop

# Simple program that runs until explicitly stopped
while True:
    command = input("Enter command (type 'exit' to quit): ")
    
    if command.lower() == 'exit':
        print("Exiting program...")
        break
    
    # Process other commands
    print(f"You entered: {command}")

Here, the while True creates a deliberate infinite loop that continues until the user enters 'exit', triggering the break statement.

Common Causes of Unintentional Infinite Loops

  1. Forgetting to update the loop variable
    # Infinite loop - count never changes!
    count = 1
    while count <= 5:
        print(f"Count is: {count}")
        # Missing: count += 1
  2. Condition that's always true
    # Infinite loop - condition is always true!
    x = 10
    while x > 5:
        print("x is greater than 5")
        # x is never decreased
  3. Logical errors in condition updates
    # Infinite loop - wrong direction!
    i = 10
    while i > 0:
        print(i)
        i += 1  # Increasing instead of decreasing!

Safeguards Against Infinite Loops

  1. Always verify your loop variable update - Ensure it's moving toward the termination condition
  2. Add a safety counter - Implement a maximum iteration count as a fallback
    max_iterations = 1000
    iteration = 0
    
    while some_condition:
        # Loop body
        
        iteration += 1
        if iteration >= max_iterations:
            print("Maximum iterations reached. Breaking loop.")
            break
  3. Use debugging print statements - To track the value of key variables
  4. Test termination conditions separately - Before implementing the loop

Remember: if you do get stuck in an infinite loop while running a Python script, you can typically terminate it by pressing Ctrl+C (or Cmd+C on Mac).

Nested Loops

A nested loop is a loop inside another loop. The inner loop completes all its iterations for each single iteration of the outer loop.

Basic Structure

for outer_item in outer_sequence:
    # Outer loop code
    
    for inner_item in inner_sequence:
        # Inner loop code
        # This runs completely for each iteration of the outer loop

Think of nested loops like the hands on a clock: for each hour (outer loop), the minute hand makes a complete 60-minute cycle (inner loop).

Example: Multiplication Table

# Generating a multiplication table
for i in range(1, 6):  # Outer loop: 1 to 5
    for j in range(1, 6):  # Inner loop: 1 to 5
        product = i * j
        print(f"{i} × {j} = {product}", end="\t")
    print()  # New line after each row

# Output:
# 1 × 1 = 1    1 × 2 = 2    1 × 3 = 3    1 × 4 = 4    1 × 5 = 5
# 2 × 1 = 2    2 × 2 = 4    2 × 3 = 6    2 × 4 = 8    2 × 5 = 10
# 3 × 1 = 3    3 × 2 = 6    3 × 3 = 9    3 × 4 = 12   3 × 5 = 15
# 4 × 1 = 4    4 × 2 = 8    4 × 3 = 12   4 × 4 = 16   4 × 5 = 20
# 5 × 1 = 5    5 × 2 = 10   5 × 3 = 15   5 × 4 = 20   5 × 5 = 25

Here, for each value of i, the inner loop goes through all values of j, creating a complete row of the table.

Pattern Printing with Nested Loops

# Printing a right-angled triangle pattern
rows = 5

for i in range(1, rows + 1):
    for j in range(1, i + 1):
        print("*", end=" ")
    print()  # New line after each row

# Output:
# * 
# * * 
# * * * 
# * * * * 
# * * * * *

This example demonstrates how nested loops are essential for creating patterns where each row has a different number of elements.

Real-World Example: Processing Multi-dimensional Data

student_grades = [
    ["Alice", [85, 90, 92, 88]],
    ["Bob", [75, 82, 79, 84]],
    ["Charlie", [95, 97, 91, 93]],
    ["Diana", [70, 65, 72, 69]]
]

print("Student Grade Analysis")
print("-" * 30)

for student, grades in student_grades:
    total = 0
    highest = 0
    lowest = 100
    
    print(f"{student}'s grades:")
    
    for index, grade in enumerate(grades, start=1):
        print(f"  Test {index}: {grade}")
        total += grade
        highest = max(highest, grade)
        lowest = min(lowest, grade)
    
    average = total / len(grades)
    print(f"  Average: {average:.2f}")
    print(f"  Highest: {highest}")
    print(f"  Lowest: {lowest}")
    print()

# Output:
# Student Grade Analysis
# ------------------------------
# Alice's grades:
#   Test 1: 85
#   Test 2: 90
#   Test 3: 92
#   Test 4: 88
#   Average: 88.75
#   Highest: 92
#   Lowest: 85
#
# Bob's grades:
#   Test 1: 75
#   Test 2: 82
#   Test 3: 79
#   Test 4: 84
#   Average: 80.00
#   Highest: 84
#   Lowest: 75
# ...etc.

This example shows how nested loops are essential for processing multi-dimensional data structures, like records containing lists.

Performance Considerations with Nested Loops

Nested loops multiply the number of iterations. With two loops of sizes n and m, the inner code runs n×m times. This can become performance-intensive with large datasets.

# O(n²) time complexity with nested loops
n = 1000

# This inner code will run 1,000,000 times!
for i in range(n):
    for j in range(n):
        # Do something
        pass

Always consider alternatives when working with large datasets:

List Comprehensions: Compact Loop Alternatives

List comprehensions provide a concise way to create lists based on existing sequences. They combine a for loop and a list creation into a single line.

Basic Syntax

[expression for item in iterable if condition]

This creates a new list by applying an expression to each item in an iterable that satisfies an optional condition.

Simple List Comprehension Examples

# Traditional for loop
squares = []
for i in range(1, 11):
    squares.append(i ** 2)

# Equivalent list comprehension
squares = [i ** 2 for i in range(1, 11)]
print(squares)  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# With a condition (only even numbers)
even_squares = [i ** 2 for i in range(1, 11) if i % 2 == 0]
print(even_squares)  # [4, 16, 36, 64, 100]

List comprehensions aren't just shorter—they're often faster and more readable once you're familiar with the syntax.

More Complex Examples

# Flattening a 2D list
matrix = [[1, 2, 3], [4, a5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
print(flattened)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Creating a dictionary with comprehension
squares_dict = {i: i**2 for i in range(1, 6)}
print(squares_dict)  # {1: 1, 2: 4, 3: 9, 4: a16, 5: 25}

# Filtering and transforming strings
words = ["apple", "banana", "cherry", "date", "elderberry"]
long_words_upper = [word.upper() for word in words if len(word) > 5]
print(long_words_upper)  # ['BANANA', 'CHERRY', 'ELDERBERRY']

The nested loop syntax in list comprehensions ([item for sublist in list for item in sublist]) follows the same order as actual nested loops.

When to Use List Comprehensions

List comprehensions shine when:

However, they can become hard to read with complex operations. In such cases, traditional loops often provide better readability.

Real-World Example: Data Cleaning with Comprehensions

raw_data = [
    "42",
    "N/A",
    "37.5",
    "",
    "error",
    "19",
    "41.8",
    "not recorded"
]

# Extract and convert all valid numeric values
cleaned_data = [float(value) for value in raw_data 
                if value.replace(".", "", 1).isdigit()]

print(cleaned_data)  # [42.0, 37.5, 19.0, 41.8]

# Calculate statistics
if cleaned_data:
    average = sum(cleaned_data) / len(cleaned_data)
    minimum = min(cleaned_data)
    maximum = max(cleaned_data)
    
    print(f"Average: {average:.2f}")
    print(f"Minimum: {minimum:.2f}")
    print(f"Maximum: {maximum:.2f}")
    print(f"Valid readings: {len(cleaned_data)}/{len(raw_data)}")

This example shows how a list comprehension can elegantly handle data cleaning and filtering in a single line.

Common Loop Patterns and Techniques

Certain loop patterns appear frequently in programming. Recognizing these patterns can help you solve problems more efficiently.

Accumulation Pattern

This pattern builds a result by updating a variable in each iteration.

# Sum accumulation
numbers = [1, 2, 3, 4, 5]
total = 0

for num in numbers:
    total += num
    
print(total)  # 15

# String accumulation
words = ["Python", "is", "awesome"]
sentence = ""

for word in words:
    sentence += word + " "
    
print(sentence.strip())  # "Python is awesome"

Filtering Pattern

This pattern selects elements from a sequence based on a condition.

# Traditional filtering
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = []

for num in numbers:
    if num % 2 == 0:
        evens.append(num)
        
print(evens)  # [2, 4, 6, 8, 10]

# With list comprehension
evens = [num for num in numbers if num % 2 == 0]

Mapping Pattern

This pattern transforms each element in a sequence to create a new sequence.

# Traditional mapping
numbers = [1, 2, 3, 4, 5]
squared = []

for num in numbers:
    squared.append(num ** 2)
    
print(squared)  # [1, 4, 9, 16, 25]

# With list comprehension
squared = [num ** 2 for num in numbers]

Search Pattern

This pattern looks for an element in a sequence that meets a specific condition.

numbers = [4, 7, 2, 9, 3, 1, 8]
target = 9
found = False
position = -1

for i, num in enumerate(numbers):
    if num == target:
        found = True
        position = i
        break
        
if found:
    print(f"Found {target} at position {position}.")
else:
    print(f"{target} not found in the list.")

Parallel Iteration Pattern

This pattern processes multiple sequences simultaneously.

names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]

# Using zip to iterate over multiple sequences together
for name, score in zip(names, scores):
    print(f"{name}: {score}")

# Output:
# Alice: 85
# Bob: 92
# Charlie: 78

Counting Pattern

This pattern counts occurrences that meet specific criteria.

text = "Python programming is fun and Python is easy to learn"
words = text.split()
python_count = 0

for word in words:
    if word.lower() == "python":
        python_count += 1
        
print(f"'Python' appears {python_count} times in the text.")

Max/Min Pattern

This pattern finds the maximum or minimum value in a sequence.

temperatures = [72, 68, 75, 63, 80, 67, 78]
highest = temperatures[0]  # Initialize with first value
lowest = temperatures[0]

for temp in temperatures[1:]:  # Start from the second item
    if temp > highest:
        highest = temp
    if temp < lowest:
        lowest = temp
        
print(f"Highest temperature: {highest}°F")
print(f"Lowest temperature: {lowest}°F")

Real-World Example: Multiple Patterns in a Data Analysis Task

sales_data = [
    {"date": "2023-01-15", "product": "Laptop", "amount": 1200, "units": 1},
    {"date": "2023-01-16", "product": "Mouse", "amount": 25, "units": 5},
    {"date": "2023-01-16", "product": "Laptop", "amount": 2400, "units": 2},
    {"date": "2023-01-17", "product": "Keyboard", "amount": 80, "units": 2},
    {"date": "2023-01-18", "product": "Mouse", "amount": 30, "units": 6},
    {"date": "2023-01-19", "product": "Monitor", "amount": 350, "units": 1},
    {"date": "2023-01-20", "product": "Laptop", "amount": 1200, "units": 1}
]

# Analysis goals:
# 1. Total sales amount
# 2. Count the number of each product sold
# 3. Find the day with highest sales
# 4. Calculate average sale amount per transaction

# Accumulation for total sales
total_sales = 0

# Counting occurrences for each product
product_counts = {}

# Finding max for day with highest sales
best_day = {"date": "", "amount": 0}

# Prepare for average calculation
transaction_count = len(sales_data)

# Main loop with multiple patterns
for sale in sales_data:
    # Accumulation pattern for total
    total_sales += sale["amount"]
    
    # Counting pattern for products
    product = sale["product"]
    if product in product_counts:
        product_counts[product] += sale["units"]
    else:
        product_counts[product] = sale["units"]
    
    # Max pattern for best day
    if sale["amount"] > best_day["amount"]:
        best_day["date"] = sale["date"]
        best_day["amount"] = sale["amount"]

# Calculate average (not in the loop)
average_sale = total_sales / transaction_count

# Output results
print(f"Total sales: ${total_sales}")
print("\nProduct units sold:")
for product, count in product_counts.items():
    print(f"  {product}: {count} units")
print(f"\nBest sales day: {best_day['date']} (${best_day['amount']})")
print(f"Average sale amount: ${average_sale:.2f}")

This example demonstrates how multiple loop patterns can be combined to perform a complete data analysis task.

Best Practices and Optimizations

Here are some guidelines to write efficient and readable loops in Python:

Choose the Right Loop Type

Loop Efficiency

Readability Over Cleverness

Alternative Approaches to Traditional Loops

Python provides several functions that can sometimes replace loops with more readable alternatives:

Real-World Optimization Example

import time

# Sample task: Count words in a large text that start with each letter
text = "This is a sample text. " * 1000000  # 5 million words
words = text.split()

# Approach 1: Nested loops (inefficient)
def count_first_letters_inefficient(word_list):
    start_time = time.time()
    
    alphabet = "abcdefghijklmnopqrstuvwxyz"
    counts = {}
    
    for letter in alphabet:
        count = 0
        for word in word_list:
            if word.lower().startswith(letter):
                count += 1
        counts[letter] = count
    
    end_time = time.time()
    return counts, end_time - start_time

# Approach 2: Single pass with dictionary (efficient)
def count_first_letters_efficient(word_list):
    start_time = time.time()
    
    counts = {}
    
    for word in word_list:
        first_letter = word[0].lower()
        if first_letter.isalpha():
            counts[first_letter] = counts.get(first_letter, 0) + 1
    
    end_time = time.time()
    return counts, end_time - start_time

# Compare performance
inefficient_counts, inefficient_time = count_first_letters_inefficient(words)
efficient_counts, efficient_time = count_first_letters_efficient(words)

print(f"Inefficient approach time: {inefficient_time:.2f} seconds")
print(f"Efficient approach time: {efficient_time:.2f} seconds")
print(f"Speedup factor: {inefficient_time / efficient_time:.2f}x")

This example demonstrates how rewriting a nested loop as a single-pass algorithm with a suitable data structure can dramatically improve performance with large datasets.

Practice Exercises

To solidify your understanding of loops, try these exercises. Solutions will be reviewed in class.

  1. Basic For Loop: Write a program that calculates the sum of all even numbers from 1 to 100 using a for loop.

  2. While Loop with User Input: Write a guessing game program where the computer randomly selects a number between 1 and 100, and the user keeps guessing until they get it right. Provide "higher" or "lower" hints after each guess.

  3. Nested Loops: Print a pyramid pattern of asterisks with a given number of rows:

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

  4. Loop Control: Write a program that prints all prime numbers between 1 and 50.

  5. List Comprehension: Convert a list of temperatures in Celsius to Fahrenheit using a list comprehension. (Formula: F = C * 9/5 + 32)

  6. Dictionary Loop: Given a string, create a dictionary that counts how many times each character appears in the string.

  7. Advanced Challenge: Write a function that checks if a given number is a "perfect number" (equal to the sum of its proper divisors, e.g., 6 = 1+2+3).

Real-World Applications of Loops

Loops are foundational to numerous real-world programming tasks. Here are some practical applications:

Example: Web Scraping with Loops

import requests
from bs4 import BeautifulSoup

# Function to extract product details from a page
def extract_products(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.text, 'html.parser')
    products = []
    
    # Find all product containers
    product_containers = soup.find_all('div', class_='product-item')
    
    for container in product_containers:
        name = container.find('h3', class_='product-name').text.strip()
        price = container.find('span', class_='price').text.strip()
        rating = container.find('div', class_='rating').get('data-rating')
        
        products.append({
            'name': name,
            'price': price,
            'rating': float(rating)
        })
    
    return products

# Main program to scrape multiple pages
base_url = "https://example-store.com/products?page="
all_products = []
max_pages = 5

for page_num in range(1, max_pages + 1):
    print(f"Scraping page {page_num}...")
    page_url = base_url + str(page_num)
    
    # Get products from this page
    page_products = extract_products(page_url)
    all_products.extend(page_products)
    
    # Stop if we reach a page with no products
    if not page_products:
        print(f"No more products found on page {page_num}. Stopping.")
        break
    
    # Be polite to the server
    time.sleep(1)

print(f"Scraped {len(all_products)} products total.")

# Find the highest rated products
highest_rated = []
highest_rating = 0

for product in all_products:
    if product['rating'] > highest_rating:
        highest_rated = [product]
        highest_rating = product['rating']
    elif product['rating'] == highest_rating:
        highest_rated.append(product)

print(f"\nHighest rated products (Rating: {highest_rating}):")
for product in highest_rated:
    print(f"- {product['name']} ({product['price']})")

This example demonstrates how loops are essential for web scraping tasks, including pagination handling and data processing.

Conclusion and Next Steps

We've covered the fundamental concepts and techniques of Python loops, from basic for and while loops to advanced patterns and optimizations. Loops are one of the core building blocks that allow us to automate repetitive tasks, process data efficiently, and create dynamic programs.

Tomorrow, we'll continue building on these control flow concepts by exploring data structures in Python. You'll learn how to combine loops with lists, dictionaries, and other collections to solve even more complex problems.

Remember to practice these loop concepts by working through the exercises. Mastering loops is essential for becoming proficient in Python and programming in general.

For further exploration, consider reading the official Python documentation on control flow statements: https://docs.python.org/3/tutorial/controlflow.html