Python Loop Control: break, continue, pass

Week 2 Day 2: Control Flow Fundamentals

Introduction to Loop Control

Welcome to our exploration of loop control statements in Python! While loops give us the power to repeat code, loop control statements give us the finesse to decide exactly when and how those repetitions occur.

Think of loops as vehicles traveling along a path. Without control mechanisms, they would simply follow the route from beginning to end the specified number of times. Loop control statements are like the steering wheel, brakes, and accelerator that let you navigate more precisely—taking detours, skipping sections, or stopping the journey entirely when needed.

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

Loop Control Mechanisms in Python

Python provides three primary mechanisms for controlling the flow of loops:

These control mechanisms give you precise control over how your loops execute, allowing you to create more efficient and readable code by avoiding unnecessary iterations or handling special cases elegantly.

The break Statement: Exiting Loops Early

The break statement immediately terminates the loop it's in, transferring control to the first statement after the loop. It's like an emergency exit that lets you escape from a loop when a certain condition is met.

Basic Syntax

for item in sequence:
    # Some code
    if condition:
        break
    # More code

# Execution continues here after break

Think of break as a fire alarm in a building—when triggered, everyone immediately leaves the building (the loop) and goes to the designated meeting point (the next statement after the loop).

Simple Example: Finding an Element

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

for num in numbers:
    print(f"Checking {num}")
    if num == target:
        print(f"Found the target: {num}")
        break
    print("Not the target, continuing search...")

print("Search complete")

# Output:
# Checking 4
# Not the target, continuing search...
# Checking 7
# Not the target, continuing search...
# Checking 2
# Not the target, continuing search...
# Checking 9
# Found the target: 9
# Search complete

Notice how the loop stops as soon as the target is found, without checking the remaining elements. This makes the search more efficient.

Using break with while Loops

print("Enter 'quit' to exit")

while True:  # Infinite loop
    user_input = input("Enter a word: ")
    
    if user_input.lower() == 'quit':
        print("Exiting program...")
        break
    
    print(f"You entered: {user_input}")

print("Program ended")

This creates an infinite loop that keeps asking for input until the user enters 'quit'. The break statement provides a controlled way to exit the otherwise endless loop.

Real-World Example: Processing a Large File

def process_log_until_error(log_file_path):
    """Process a log file until an error entry is found."""
    try:
        with open(log_file_path, 'r') as file:
            line_number = 0
            for line in file:
                line_number += 1
                
                # Process the line
                print(f"Processing line {line_number}: {line.strip()}")
                
                # Check if the line contains an error
                if "ERROR" in line:
                    print(f"Error found at line {line_number}. Stopping processing.")
                    break
                
                # Normally we would do more processing here
                
            print("Finished processing the log file.")
    except FileNotFoundError:
        print(f"Could not find the log file: {log_file_path}")

# Example usage
process_log_until_error("server.log")

In this example, we're processing a log file line by line but stop as soon as we find an error. This is useful when you're troubleshooting and only care about the first error that occurred.

Break in Nested Loops

An important characteristic of break is that it only exits the innermost loop that contains it. If you need to exit multiple levels of nested loops, you'll need additional techniques.

for i in range(3):
    print(f"Outer loop: i = {i}")
    
    for j in range(3):
        print(f"  Inner loop: j = {j}")
        
        if i == 1 and j == 1:
            print("  Breaking inner loop")
            break
            
    print("Outer loop continues")

# Output:
# Outer loop: i = 0
#   Inner loop: j = 0
#   Inner loop: j = 1
#   Inner loop: j = 2
# Outer loop continues
# Outer loop: i = 1
#   Inner loop: j = 0
#   Inner loop: j = 1
#   Breaking inner loop
# Outer loop continues
# Outer loop: i = 2
#   Inner loop: j = 0
#   Inner loop: j = 1
#   Inner loop: j = 2
# Outer loop continues

Notice how the break only exits the inner loop, and the outer loop continues to its next iteration.

Breaking Out of Multiple Loops

If you need to exit multiple nested loops at once, one approach is to use a flag variable:

found = False  # Flag to control outer loop

for i in range(5):
    print(f"Outer loop: i = {i}")
    
    for j in range(5):
        print(f"  Inner loop: j = {j}")
        
        if i == 2 and j == 3:
            print("  Target found! Breaking out of all loops.")
            found = True
            break
    
    if found:
        break

print("Both loops have exited")

Another approach is to encapsulate the nested loops in a function and use return to exit all loops at once:

def search_2d_matrix(matrix, target):
    """Search for target in a 2D matrix and return position if found."""
    for i in range(len(matrix)):
        for j in range(len(matrix[i])):
            print(f"Checking position ({i}, {j}): {matrix[i][j]}")
            
            if matrix[i][j] == target:
                print(f"Found {target} at position ({i}, {j})")
                return (i, j)  # Exits the entire function, effectively breaking both loops
    
    print(f"{target} not found in the matrix")
    return None

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

position = search_2d_matrix(matrix, 5)
print(f"Result: {position}")

The continue Statement: Skipping Iterations

The continue statement skips the rest of the current iteration and immediately jumps to the next iteration of the loop. It's like hitting the "skip" button when listening to a playlist—you bypass the current song without stopping the entire playlist.

Basic Syntax

for item in sequence:
    # Some code
    if condition:
        continue
    # This code is skipped when continue is executed
    
# Loop continues with the next iteration

Think of continue as a detour sign on a road—it redirects you away from one path (the rest of the current iteration) and back to the main road (the start of the next iteration).

Simple Example: Skipping Values

for num in range(1, 11):
    if num % 2 == 0:  # If number is even
        continue
    
    print(num)

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

Here, the continue statement skips even numbers, so only odd numbers are printed.

Cleaning Data with continue

raw_data = ["apple", "", "banana", None, "cherry", "", "date"]
clean_data = []

for item in raw_data:
    # Skip empty or None values
    if not item:
        print(f"Skipping invalid data: {item!r}")
        continue
    
    # Process valid data
    processed_item = item.strip().capitalize()
    clean_data.append(processed_item)
    print(f"Processed: {processed_item}")

print(f"Clean data: {clean_data}")

# Output:
# Processed: Apple
# Skipping invalid data: ''
# Processed: Banana
# Skipping invalid data: None
# Processed: Cherry
# Skipping invalid data: ''
# Processed: Date
# Clean data: ['Apple', 'Banana', 'Cherry', 'Date']

In this example, continue helps us skip invalid data items without complicating our processing logic with nested conditionals.

Using continue with while Loops

count = 0

while count < 10:
    count += 1
    
    if count % 3 == 0:
        print(f"Skipping {count} (divisible by 3)")
        continue
        
    print(f"Processing {count}")

# Output:
# Processing 1
# Processing 2
# Skipping 3 (divisible by 3)
# Processing 4
# Processing 5
# Skipping 6 (divisible by 3)
# Processing 7
# Processing 8
# Skipping 9 (divisible by 3)
# Processing 10

With while loops, it's especially important to ensure that the loop condition can eventually become False, even when using continue. In this example, we increment count before the continue statement to avoid an infinite loop.

Real-World Example: Web Scraping

def scrape_articles(urls):
    """
    Scrape articles from a list of URLs, handling various edge cases.
    """
    articles = []
    
    for url in urls:
        print(f"Processing: {url}")
        
        # Skip invalid URLs
        if not url.startswith('http'):
            print(f"Skipping invalid URL: {url}")
            continue
            
        # Simulate fetching the content
        try:
            content = fetch_content(url)  # This would be a real function in production
        except Exception as e:
            print(f"Error fetching {url}: {e}")
            continue
            
        # Skip if no content was retrieved
        if not content:
            print(f"No content found at {url}")
            continue
            
        # Process the content
        article = {
            'url': url,
            'title': extract_title(content),
            'text': extract_text(content),
            'date': extract_date(content)
        }
        
        articles.append(article)
        print(f"Successfully scraped article: {article['title']}")
    
    return articles

# Simulate the helper functions for the example
def fetch_content(url):
    # Simulate successful/failed fetches
    if 'example.com' in url:
        return f"Content from {url}"
    return None

def extract_title(content):
    return f"Title from {content}"

def extract_text(content):
    return f"Text from {content}"

def extract_date(content):
    return "2023-01-01"

# Example usage
urls = [
    'http://example.com/article1',
    'invalid-url',
    'http://example.com/article2',
    'http://broken-site.com/article'
]

articles = scrape_articles(urls)

In this web scraping example, continue allows us to gracefully skip URLs that are invalid, fail to fetch, or don't contain the expected content. This creates a more robust scraper that can handle real-world edge cases without crashing.

Combining break and continue

You can use both break and continue in the same loop for different conditions:

numbers = [1, -2, 3, -4, 0, 5, -6, 7, 8]
sum_positive = 0

for num in numbers:
    # Break if we encounter zero
    if num == 0:
        print("Found zero! Stopping summation.")
        break
        
    # Skip negative numbers
    if num < 0:
        print(f"Skipping negative number: {num}")
        continue
        
    # Process positive numbers
    sum_positive += num
    print(f"Adding {num} to sum, current total: {sum_positive}")

print(f"Final sum of positive numbers: {sum_positive}")

# Output:
# Adding 1 to sum, current total: 1
# Skipping negative number: -2
# Adding 3 to sum, current total: 4
# Skipping negative number: -4
# Found zero! Stopping summation.
# Final sum of positive numbers: 4

This example shows how break and continue can work together: continue skips negative numbers, and break stops the entire calculation if zero is encountered.

The pass Statement: Doing Nothing

The pass statement is a null operation—it does nothing. Unlike break and continue, which affect the flow of a loop, pass simply acts as a placeholder when syntax requires a statement but you don't want to execute any code.

Basic Syntax

for item in sequence:
    if condition:
        pass  # Does nothing
    else:
        # Do something

Think of pass as a "no parking" sign—it occupies a space without actually doing anything there.

Using pass as a Placeholder

# Function stub to be implemented later
def process_data(data):
    pass  # To be implemented

# Conditional placeholder
age = 25
if age < 18:
    pass  # Handle minors (TODO)
else:
    print("Access granted for adult")

The pass statement allows you to create syntactically correct code blocks that you plan to implement later. It's especially useful during development when you're sketching out the structure of your program.

Empty Loops with pass

# Find all even numbers (but don't do anything with them yet)
for num in range(1, 10):
    if num % 2 == 0:
        pass  # TODO: Collect even numbers
    else:
        print(f"{num} is odd")

Here, pass allows us to acknowledge even numbers without taking any action yet. This can be useful when you're prototyping and want to focus on one part of your logic first.

Using pass in Exception Handling

def safe_divide(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        # Silently handle division by zero
        pass
    
    # Default return value if exception occurs
    return None

print(safe_divide(10, 2))  # 5.0
print(safe_divide(10, 0))  # None

In this example, pass allows us to silently handle a division by zero without executing any specific code in the exception handler. This creates a "fail-soft" behavior where the function returns None instead of crashing.

Empty Class Definition

# Define a class with no attributes or methods yet
class EmptyPlaceholder:
    pass

# Empty classes can still be instantiated
obj = EmptyPlaceholder()

When designing class hierarchies, pass allows you to define classes that don't yet have any implementation but will be filled in later.

Real-World Example: State Machine

def process_order(order, state):
    """
    Process an order based on its current state.
    Returns the new state after processing.
    """
    if state == "new":
        print(f"Processing new order: {order['id']}")
        # Validate order details
        if not order.get('items'):
            return "error"
        return "processing"
        
    elif state == "processing":
        print(f"Order {order['id']} is being processed")
        # Check inventory and payment
        if order.get('payment_status') != 'complete':
            print(f"Payment pending for order {order['id']}")
            return "payment_pending"
        return "ready_to_ship"
        
    elif state == "payment_pending":
        # This state is handled by a different subsystem
        pass
        
    elif state == "ready_to_ship":
        print(f"Order {order['id']} is ready to ship")
        # Arrange shipping
        return "shipped"
        
    elif state == "shipped":
        print(f"Order {order['id']} has been shipped")
        # Nothing to do until delivery confirmation
        pass
        
    elif state == "error":
        print(f"Order {order['id']} has errors that must be resolved manually")
        # Log the error for manual review
        pass
        
    # Return the unchanged state if no transition occurred
    return state

# Example usage
order = {
    'id': '12345',
    'items': ['product1', 'product2'],
    'payment_status': 'complete'
}

current_state = "new"
while current_state != "shipped":
    print(f"Current state: {current_state}")
    current_state = process_order(order, current_state)
    print(f"New state: {current_state}")
    print("-" * 30)
    
    # Prevent infinite loop in this example
    if current_state == "error" or current_state == "payment_pending":
        break

In this state machine example, pass serves as a placeholder for states where the current function doesn't need to take any action. This might be because the action is handled by another system or because that state is simply a waiting state with no processing required.

Common Patterns and Techniques

Now that we've explored each control statement individually, let's look at some common patterns and techniques that use these statements effectively.

Early Exit Pattern

The early exit pattern uses break or return to exit a loop as soon as a solution is found, avoiding unnecessary iterations.

def find_first_prime(numbers):
    """Find the first prime number in a list."""
    for num in numbers:
        # Skip non-positive numbers
        if num <= 1:
            continue
            
        # Check if num is prime
        is_prime = True
        for i in range(2, int(num**0.5) + 1):
            if num % i == 0:
                is_prime = False
                break
                
        if is_prime:
            return num  # Early exit when we find a prime
            
    return None  # No prime number found

numbers = [4, 6, 8, 9, 11, 13, 15]
prime = find_first_prime(numbers)
print(f"First prime number: {prime}")  # 11

This pattern is efficient because it stops searching as soon as it finds what it's looking for, rather than checking all elements.

Skip and Filter Pattern

The skip and filter pattern uses continue to bypass elements that don't meet specific criteria.

def analyze_sensor_data(readings):
    """
    Analyze sensor readings, filtering out invalid values.
    """
    valid_readings = []
    sum_valid = 0
    count_valid = 0
    min_valid = float('inf')
    max_valid = float('-inf')
    
    for reading in readings:
        # Skip None values
        if reading is None:
            print("Skipping None reading")
            continue
            
        # Skip negative values (impossibilities for this sensor)
        if reading < 0:
            print(f"Skipping invalid negative reading: {reading}")
            continue
            
        # Skip outliers (values outside the expected range)
        if reading > 100:
            print(f"Skipping outlier reading: {reading}")
            continue
            
        # Process valid readings
        valid_readings.append(reading)
        sum_valid += reading
        count_valid += 1
        min_valid = min(min_valid, reading)
        max_valid = max(max_valid, reading)
    
    # Calculate statistics if we have valid readings
    if count_valid > 0:
        avg_valid = sum_valid / count_valid
        return {
            'valid_readings': valid_readings,
            'count': count_valid,
            'min': min_valid,
            'max': max_valid,
            'average': avg_valid
        }
    else:
        return {'error': 'No valid readings found'}

# Example usage
sensor_data = [23.5, None, 18.2, -3.7, 105.9, 62.1, None, 74.3]
result = analyze_sensor_data(sensor_data)
print(result)

This pattern allows us to gracefully handle and skip invalid data without cluttering our processing logic with nested conditionals.

State Processing Pattern

This pattern uses different loop control mechanisms based on the state or phase of processing.

def process_transaction_batch(transactions):
    """
    Process a batch of financial transactions,
    handling different states appropriately.
    """
    successful = []
    failed = []
    skipped = []
    
    # Initial safety check
    if not transactions:
        return {'error': 'No transactions provided'}
    
    # Process each transaction
    for tx in transactions:
        print(f"Processing transaction {tx['id']}: ${tx['amount']}")
        
        # Check transaction state and handle accordingly
        if tx['state'] == 'completed':
            print(f"Transaction {tx['id']} already completed, skipping")
            skipped.append(tx)
            continue
            
        elif tx['state'] == 'pending':
            # Process pending transaction
            if tx['amount'] <= 0:
                print(f"Invalid amount ${tx['amount']} for transaction {tx['id']}")
                tx['state'] = 'failed'
                tx['error'] = 'Invalid amount'
                failed.append(tx)
                continue
                
            if tx['balance'] < tx['amount']:
                print(f"Insufficient funds for transaction {tx['id']}")
                tx['state'] = 'failed'
                tx['error'] = 'Insufficient funds'
                failed.append(tx)
                continue
                
            # Successfully process the transaction
            print(f"Successfully processed transaction {tx['id']}")
            tx['state'] = 'completed'
            successful.append(tx)
            
        elif tx['state'] == 'failed':
            print(f"Transaction {tx['id']} previously failed, skipping")
            skipped.append(tx)
            continue
            
        else:
            print(f"Unknown state '{tx['state']}' for transaction {tx['id']}")
            tx['state'] = 'failed'
            tx['error'] = 'Unknown state'
            failed.append(tx)
    
    # Return the results of batch processing
    return {
        'successful': successful,
        'failed': failed,
        'skipped': skipped,
        'total_processed': len(successful) + len(failed),
        'total_successful': len(successful),
        'total_failed': len(failed),
        'total_skipped': len(skipped)
    }

# Example transactions
transactions = [
    {'id': 'TX001', 'amount': 50.0, 'balance': 100.0, 'state': 'pending'},
    {'id': 'TX002', 'amount': 25.0, 'balance': 20.0, 'state': 'pending'},
    {'id': 'TX003', 'amount': -10.0, 'balance': 500.0, 'state': 'pending'},
    {'id': 'TX004', 'amount': 100.0, 'balance': 300.0, 'state': 'completed'},
    {'id': 'TX005', 'amount': 75.0, 'balance': 200.0, 'state': 'pending'},
    {'id': 'TX006', 'amount': 40.0, 'balance': 50.0, 'state': 'failed'}
]

result = process_transaction_batch(transactions)
print("\nBatch processing results:")
print(f"Successful: {result['total_successful']}")
print(f"Failed: {result['total_failed']}")
print(f"Skipped: {result['total_skipped']}")

This pattern demonstrates how continue can be used to skip transactions that don't need processing, making the code more readable by avoiding deeply nested conditionals.

Infinite Loop with break

This pattern creates an intentional infinite loop that continues until a specific condition triggers a break.

def simple_calculator():
    """A simple calculator that runs until the user chooses to exit."""
    print("Simple Calculator")
    print("Enter 'q' to quit at any time")
    
    while True:  # Intentional infinite loop
        # Get first number
        num1_input = input("\nEnter first number: ")
        if num1_input.lower() == 'q':
            break
            
        try:
            num1 = float(num1_input)
        except ValueError:
            print("Invalid number. Please try again.")
            continue
        
        # Get operation
        operation = input("Enter operation (+, -, *, /): ")
        if operation.lower() == 'q':
            break
            
        if operation not in ['+', '-', '*', '/']:
            print("Invalid operation. Please try again.")
            continue
        
        # Get second number
        num2_input = input("Enter second number: ")
        if num2_input.lower() == 'q':
            break
            
        try:
            num2 = float(num2_input)
        except ValueError:
            print("Invalid number. Please try again.")
            continue
        
        # Perform calculation
        if operation == '+':
            result = num1 + num2
        elif operation == '-':
            result = num1 - num2
        elif operation == '*':
            result = num1 * num2
        elif operation == '/':
            if num2 == 0:
                print("Error: Division by zero")
                continue
            result = num1 / num2
        
        print(f"Result: {num1} {operation} {num2} = {result}")
    
    print("Calculator closed. Goodbye!")

# Example usage
# simple_calculator()

This pattern is useful for interactive programs that should continue running until the user explicitly chooses to exit.

Best Practices and Guidelines

Let's explore some best practices for using loop control statements effectively and responsibly.

When to Use break

When to Use continue

When to Use pass

Common Pitfalls to Avoid

Balancing Readability and Efficiency

# Less readable approach with deeply nested conditionals
def process_items(items):
    results = []
    for item in items:
        if item is not None:
            if isinstance(item, (int, float)):
                if item > 0:
                    if item < 100:
                        results.append(item * 2)
    return results

# More readable approach with continue statements
def process_items_better(items):
    results = []
    for item in items:
        if item is None:
            continue
        if not isinstance(item, (int, float)):
            continue
        if item <= 0:
            continue
        if item >= 100:
            continue
            
        results.append(item * 2)
    return results

Using continue for filtering can create flatter, more readable code compared to deeply nested conditionals.

Practice Exercises

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

  1. Basic break: Write a function that finds the first occurrence of a target value in a list and returns its index. If the value is not found, return -1.

  2. continue Usage: Write a function that returns a list of all prime numbers within a given range.

  3. Infinite Loop with break: Create a simple number guessing game that continues until the user guesses the correct number or types 'quit'.

  4. Data Cleaning: Write a function that processes a list of values, skipping None values and negatives, and returns the average of the valid values.

  5. Nested Loops with break: Create a function that finds the first pair of numbers in a list that sum to a target value and returns the indices.

  6. pass Usage: Create a basic class hierarchy with placeholder methods using pass.

  7. Advanced Challenge: Implement a simplified text parser that processes a text file line by line, skipping comments (lines starting with #) and blank lines, and breaks if it encounters a line containing 'END_OF_DATA'.

Real-World Applications

Loop control statements are essential in many real-world programming scenarios. Here are some practical applications:

Data Processing Pipeline

def process_data_batch(data_batch, config):
    """
    Process a batch of data records, handling various error conditions.
    """
    results = []
    errors = []
    skipped = 0
    
    if not data_batch:
        return {"error": "Empty batch provided"}
    
    for record in data_batch:
        record_id = record.get('id', 'unknown')
        print(f"Processing record {record_id}")
        
        # Skip records missing required fields
        required_fields = config.get('required_fields', [])
        missing_fields = [field for field in required_fields if field not in record]
        
        if missing_fields:
            print(f"Record {record_id} missing required fields: {missing_fields}")
            errors.append({
                'id': record_id,
                'error': 'Missing required fields',
                'details': missing_fields
            })
            continue
        
        # Skip records marked for exclusion
        if record.get('status') == 'exclude':
            print(f"Record {record_id} marked for exclusion, skipping")
            skipped += 1
            continue
        
        # Skip records that exceed maximum values if configured
        max_value = config.get('max_value')
        if max_value is not None and record.get('value', 0) > max_value:
            print(f"Record {record_id} exceeds maximum value ({record['value']} > {max_value})")
            errors.append({
                'id': record_id,
                'error': 'Exceeds maximum value',
                'details': {'value': record['value'], 'max': max_value}
            })
            continue
        
        # Process the valid record
        try:
            processed_record = {
                'id': record_id,
                'normalized_value': normalize_value(record.get('value', 0), config),
                'category': categorize_record(record, config),
                'timestamp': record.get('timestamp', 'unknown'),
                'metadata': extract_metadata(record, config)
            }
            
            results.append(processed_record)
            print(f"Successfully processed record {record_id}")
            
            # Stop processing if we've reached the limit
            if config.get('limit') and len(results) >= config['limit']:
                print(f"Reached processing limit of {config['limit']} records")
                break
                
        except Exception as e:
            print(f"Error processing record {record_id}: {str(e)}")
            errors.append({
                'id': record_id,
                'error': 'Processing error',
                'details': str(e)
            })
    
    return {
        'results': results,
        'errors': errors,
        'processed': len(results),
        'error_count': len(errors),
        'skipped': skipped,
        'total': len(data_batch)
    }

# Simulate helper functions
def normalize_value(value, config):
    factor = config.get('normalization_factor', 1)
    return value * factor

def categorize_record(record, config):
    categories = config.get('categories', {})
    value = record.get('value', 0)
    
    for category, range_val in categories.items():
        min_val, max_val = range_val
        if min_val <= value <= max_val:
            return category
            
    return 'uncategorized'

def extract_metadata(record, config):
    # Extract specified metadata fields
    metadata_fields = config.get('metadata_fields', [])
    return {field: record.get(field) for field in metadata_fields if field in record}

# Example usage
data = [
    {'id': '001', 'value': 150, 'timestamp': '2023-01-01', 'source': 'system1', 'quality': 'high'},
    {'id': '002', 'value': 75, 'timestamp': '2023-01-02', 'source': 'system2'},
    {'id': '003', 'status': 'exclude', 'value': 200, 'timestamp': '2023-01-03'},
    {'id': '004', 'value': 300, 'timestamp': '2023-01-04', 'source': 'system1', 'quality': 'medium'},
    {'id': '005', 'value': 50, 'source': 'system3', 'quality': 'low'},  # Missing timestamp
]

config = {
    'required_fields': ['value', 'timestamp'],
    'max_value': 250,
    'limit': 10,
    'normalization_factor': 0.1,
    'categories': {
        'low': (0, 50),
        'medium': (51, 150),
        'high': (151, 250)
    },
    'metadata_fields': ['source', 'quality']
}

result = process_data_batch(data, config)

print("\nProcessing summary:")
print(f"Processed: {result['processed']}/{result['total']} records")
print(f"Errors: {result['error_count']}")
print(f"Skipped: {result['skipped']}")

print("\nProcessed records:")
for record in result['results']:
    print(f"- {record['id']}: {record['normalized_value']} ({record['category']})")

print("\nErrors:")
for error in result['errors']:
    print(f"- {error['id']}: {error['error']} - {error['details']}")

This example demonstrates a comprehensive data processing pipeline that uses continue to skip invalid records in multiple ways and break to stop processing when a configured limit is reached.

Web Crawler with Depth Limiting

def simple_web_crawler(start_url, max_depth=2, max_pages=10):
    """
    A simple web crawler that visits links up to a maximum depth.
    """
    visited = set()
    to_visit = [(start_url, 0)]  # (url, depth)
    results = []
    
    page_count = 0
    
    while to_visit and page_count < max_pages:
        url, depth = to_visit.pop(0)
        
        # Skip if we've already visited this URL
        if url in visited:
            continue
            
        print(f"Visiting {url} (depth: {depth})")
        
        # Mark as visited
        visited.add(url)
        
        try:
            # Simulate fetching the page
            page_content = fetch_page(url)
            page_count += 1
            
            # Process the page
            title = extract_title(page_content)
            summary = generate_summary(page_content)
            
            results.append({
                'url': url,
                'title': title,
                'summary': summary,
                'depth': depth
            })
            
            # Stop if we've reached max pages
            if page_count >= max_pages:
                print(f"Reached maximum number of pages ({max_pages})")
                break
                
            # Don't extract further links if we're at max depth
            if depth >= max_depth:
                continue
                
            # Extract links and add to the queue
            links = extract_links(page_content, url)
            for link in links:
                if link not in visited:
                    to_visit.append((link, depth + 1))
                    
        except Exception as e:
            print(f"Error crawling {url}: {str(e)}")
    
    return results

# Simulate web crawling functions
def fetch_page(url):
    # Simulate fetching HTML content
    if "error" in url:
        raise Exception("Failed to fetch page")
    return f"Page at {url}Content for {url}"

def extract_title(content):
    # Simulate extracting the title
    import re
    match = re.search(r'(.*?)', content)
    return match.group(1) if match else "No title"

def generate_summary(content):
    # Simulate generating a summary
    return f"Summary of content: {content[50:100]}..."

def extract_links(content, base_url):
    # Simulate extracting links
    # In a real crawler, this would parse the HTML and find actual links
    if "leaf" in base_url:
        return []  # Leaf page, no links
        
    # Generate some fake links
    if "branch" in base_url:
        return [f"{base_url}/page{i}" for i in range(1, 4)]
        
    return [f"{base_url}/branch{i}" for i in range(1, 3)] + [f"{base_url}/leaf{i}" for i in range(1, 3)]

# Example usage
seed_url = "https://example.com"
crawl_results = simple_web_crawler(seed_url, max_depth=2, max_pages=8)

print("\nCrawl results:")
for result in crawl_results:
    print(f"{result['title']} ({result['url']}) - Depth: {result['depth']}")

This web crawler example uses continue to skip already visited URLs and pages at maximum depth, and break to stop when reaching the maximum number of pages.

Conclusion

Loop control statements—break, continue, and pass—are powerful tools that give you precise control over how your loops execute. They allow you to create more efficient and readable code by avoiding unnecessary iterations, handling special cases elegantly, and creating clean placeholders for future implementation.

To summarize what we've learned:

These control mechanisms are not just syntactic sugar—they represent fundamental patterns in programming that you'll use throughout your Python journey. As you continue learning and building more complex programs, you'll find that mastering loop control makes your code more elegant, efficient, and maintainable.

In the next lessons, we'll build on these concepts as we explore more advanced flow control structures, functions, and error handling techniques.

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