15  Loops

Loops are control structures that allow us to execute a block of code repeatedly. Each executions is called an iteration. For example, when you count from 1 to 10, you are iterating over the numbers from 1 to 10.

Code within loops can be executed either a specific number of times (definite loop) or until a certain condition is met (indefinite loop).

In Python, there are two main loop structures:

Most of these structures can be used interchangeably, but each has its own use case.

15.1 Elements of a Loop

Loops consist of three main elements:

  • Initialization. Set the initial value of the iteration variable. For example, i = 1, count = 0, etc.
  • Stop Condition. Define the condition that stops the loop. For example, i <= 10, count < 5, etc.
  • Increment/Decrement. Update the iteration variable to move to the next iteration. For example, i += 1, count += 1, etc.
What Is Happening Inside the Loop?

Loops add complexity to the program control flow. To better understand the execution sequence:

  • Track your code line by line using a debugger.
  • Check what each variable holds during execution.

15.2 Common Applications of Loops

In the following, we will cover different applications of loops in Python. Consider coming back to these examples after learning the details of each loop structure.

15.2.1 Iterating Over Lists or Collections

Loops allow you to process each element in a list or collection. For example:

  • Processing each element in a list.
  • Iterating over a range of numbers.
# List of numbers
numbers = [10, 20, 30, 40, 50]

# Iteration variable
for num in numbers:
    print("Number:", num)
Number: 10
Number: 20
Number: 30
Number: 40
Number: 50

15.2.2 Calculating Aggregates

Each iteration can update a variable to calculate the aggregate. For example:

  • Summing all numbers in a list.
  • Counting the number of elements satisfying a specific condition.
def summary_statistics(numbers):

    if len(numbers) == 0:
        return 0, 0, 0

    # Variables to store summary statistics
    total = 0
    count = 0

    for num in numbers:
        total += num
        count += 1

    average = total / count

    return count, total, average

def test_summary_statistics():
    assert summary_statistics([1, 2, 3, 4, 5]) == (5, 15, 3)
    assert summary_statistics([10, 20, 30, 40, 50]) == (5, 150, 30)
    assert summary_statistics([1, 1, 1, 1, 1]) == (5, 5, 1)
    assert summary_statistics([]) == (0, 0, 0)

test_summary_statistics()

15.2.3 Data Validation and Input

Loops can repeatedly prompt for user input until a valid value is entered. For example:

  • Repeatedly asking for numbers within a range.
  • Repeatedly asking for the correct input format of a password or list of arguments.
def data_validation_example():
    while True:
        user_input = int(input("Enter a number between 1 and 100: "))
        if 1 <= user_input <= 100:
            break
    print("Valid Input:", user_input)

Example of data validation using a while loop. The loop will continue until the user enters a number between 1 and 100.

15.2.4 Searching and Filtering

Loops can be used to search for specific elements in a collection. For example:

  • Finding the first occurrence of a value in a list.
  • Filtering elements that meet a specific condition.
  • Searching for the maximum or minimum value in a list.
  • Finding the index of a specific element in a list.
Listing 15.1: Example of finding the maximum number in a list using a loop.
def search_max_number(list):
    if len(list) == 0:
        return None
    max_number = list[0]
    for num in list:
        if num > max_number:
            max_number = num
    return max_number

def test_search_max_number():
    assert search_max_number([1, 2, 3, 4, 5]) == 5
    assert search_max_number([5, 4, 3, 2, 1]) == 5
    assert search_max_number([1, 3, 5, 2, 4]) == 5
    assert search_max_number([1, 1, 1, 1, 1]) == 1
    assert search_max_number([1]) == 1
    assert search_max_number([]) == None

test_search_max_number()

15.2.5 Generating Patterns or Sequences

With loops, the same code block can be executed multiple times to generate patterns or sequences. If the sequence obeys a logic that depends on the iteration number, loops are a good choice. For example:

  • Calculating the factorial of a number (1 * 2 * 3 * … * n).
  • Generating a sequence of prime numbers (see Listing 15.2).
Listing 15.2: Example of generating prime numbers up to 20 using a nested loop. Notice that range(2, 0) and range(2, 1) will not generate any prime numbers (range will return {python} list(range(2,0))).
def prime_numbers_until(n):
    """Generate prime numbers up to n.
    
    A prime number is a number greater than 1 that has no 
    positive divisors other than 1 and itself.

    Parameters:
    ----------
    n : int
        The upper limit of the prime numbers.

    Returns:
    -------
    list
        A list of prime numbers up to n.
    """

    prime_numbers = []
    for num in range(2, n):
        is_prime = True
        for i in range(2, num):
            if num % i == 0:
                is_prime = False
                break
        if is_prime:
            prime_numbers.append(num)
    return prime_numbers

def test_prime_numbers_until():
    assert prime_numbers_until(0) == []
    assert prime_numbers_until(1) == []
    assert prime_numbers_until(2) == []
    assert prime_numbers_until(10) == [2, 3, 5, 7]
    assert prime_numbers_until(20) == [2, 3, 5, 7, 11, 13, 17, 19]

print(list(range(2,0)))
test_prime_numbers_until()
[]

15.2.6 Repeating Actions

Loops can be used to repeat actions or apply the same operation to multiple elements. For example:

  • Sending emails to a list of recipients.
  • Applying a transformation to each item in a collection.
def send_email(email, subject, body):
    # Code to send an email
    print("Sending email to:", email)
    print("Subject:", subject)
    print("Body:", body)

def send_emails_example():
    # List of tuples containing names and emails
    contacts = [
        ("Alice", "alice@example.com"),
        ("Bob", "bob@example.com"),
        ("Charlie", "charlie@example.com"),
    ]

    for name, email in contacts:
        send_email(email, "Subject", f"Hello {name}, this is a test email.")

send_emails_example()
Sending email to: alice@example.com
Subject: Subject
Body: Hello Alice, this is a test email.
Sending email to: bob@example.com
Subject: Subject
Body: Hello Bob, this is a test email.
Sending email to: charlie@example.com
Subject: Subject
Body: Hello Charlie, this is a test email.

15.3 Looping Statements

In the following sections, we will cover the different loop structures in Python. These structures allow you to repeat a block of code a specified number of times or until a certain condition is met.

Loops Across Programming Languages

The loop structures in Python are similar to those in other programming languages. If you learn loops in Python, you will be able to apply the same concepts in other languages like Java or C++.

15.4 for Loop (Definite)

When you know in advance how many times you want to execute a block of code, you can use a for loop. In this structure, the loop configuration, namely the initialization, stop condition, and increment, are defined in a single line, at the beginning of the loop. In Figure 15.1, we have the structure of a For loop. Notice that the condition is checked at the beginning of the loop. Then, the loop body is executed if the condition is True and the iteration variable is updated before checking the condition again.

graph LR
  Start((Start))-->for
  for --> LoopStart[Initialize<br>iteration variable<br>Define Stop Condition and <br>Configure Increment]
  LoopStart-->Condition{Condition}
  Condition-->|True|Statement1["`**Loop Body**
  Statement 1
  Statement 2
  ...
  Statement N`"]
  Statement1 --> ForNext["Update iteration
                          variable"]
  ForNext --> Condition
  Condition{Condition} ---->|False| End((End))
Figure 15.1: Structure of a for loop. The loop is configured at the beginning, with the initialization, stop condition, and increment. The condition is checked at the beginning of the loop. Then, the loop body is executed if the condition is True. If the condition is False, the loop is exited. After executing the loop body, the iteration variable is updated before checking the condition again.

In the Listing 15.3, the for loop prints the numbers from 1 to 5. The range() function generates a sequence of numbers from the start value (inclusive) to the end value (exclusive). The parameters of the range() function are start, stop, and step:

  • start: The starting value of the sequence (default is 0).
  • stop: The end value of the sequence (exclusive).
  • step: The increment or decrement value (default is 1).

In Table 15.1, we show examples of using the range() function.

Table 15.1: Examples of using the range() function.
Function Call Description Output
range(5) Generates numbers from 0 to 4 0, 1, 2, 3, 4
range(1, 6) Generates numbers from 1 to 5 1, 2, 3, 4, 5
range(1, 11, 2) Generates numbers from 1 to 10, incrementing by 2 1, 3, 5, 7, 9
range(5, 0, -1) Generates numbers from 5 to 1, decrementing by 1 5, 4, 3, 2, 1
Listing 15.3: Example of a for loop that prints the numbers from 1 to 5. The loop will run as long as i is less than 6.
for i in range(1, 6):  # 1 to 5 inclusive
    # Loop body
    print("Iteration", i)
# End of loop

The elements are as follows:

  • Initialization: i = 1. The loop variable i is initialized to 1.
  • Stop Condition: i < 6. The loop will run as long as i is less than 6.
  • Increment: i += 1. The loop variable i is incremented by 1 after each iteration.

15.4.1 Using Step in for Loop

In Python, the range() function can accept a step argument, allowing you to specify the increment or decrement value. For example, range(1, 11, 2) will generate numbers from 1 to 10, incrementing by 2.

for i in range(1, 11, 2):  # Start at 1, end before 11, step by 2
    print("Iteration", i)
Iteration 1
Iteration 3
Iteration 5
Iteration 7
Iteration 9

If you want to decrement the loop variable, you can use a negative value for step.

for i in range(5, 0, -1):  # Start at 5, end before 0, step by -1
    print("Iteration", i)
Iteration 5
Iteration 4
Iteration 3
Iteration 2
Iteration 1

15.4.2 Nested Loops

Nested loops are loops within loops. They are useful when you need to perform multiple iterations based on different conditions.

When using nested loops, the inner loop will complete all its iterations before the outer loop moves to the next iteration.

For example:

for i in range(1, 4):  # Outer loop runs from 1 to 3
    for j in range(1, 3):  # Inner loop runs from 1 to 2
        print(f"Outer Loop: {i}, Inner Loop: {j}")
Outer Loop: 1, Inner Loop: 1
Outer Loop: 1, Inner Loop: 2
Outer Loop: 2, Inner Loop: 1
Outer Loop: 2, Inner Loop: 2
Outer Loop: 3, Inner Loop: 1
Outer Loop: 3, Inner Loop: 2

15.5 for ... in Loop

The for ... in loop is used to iterate through elements of a collection or iterable object. With the for loop, you can process each item in a collection without knowing its size in advance.

Listing 15.4: Example of a for ... in loop that iterates over a list of fruits. The loop will run for each element in the list and print the name of the fruit.
# Creating a list
fruits = ["Apple", "Banana", "Cherry"]

# Iteration variable
for fruit in fruits:
    print("Fruit:", fruit)
Fruit: Apple
Fruit: Banana
Fruit: Cherry

15.6 while Loop (Indefinite)

This loop allows us to repeat a block of code until a certain condition is met. It can be used when the number of iterations is not known in advance.

In Figure 15.2, we have the structure of a while loop. Notice that the condition is checked at the beginning of the loop. Then, the loop body is executed if the condition is True.

graph LR
  Start((Start)) --> LoopStart[Initialize<br>iteration variable]
  LoopStart --> While[while]
  While-->Condition{Condition}
  Condition--True-->Statement1["`**Loop Body**
  Statement 1
  Statement 2
  ...
  Statement N
  _(Update iteration variable)_`"]
  Statement1--->Condition
  Condition ---->|False| End((End))
Figure 15.2: Strucutre of a while loop. Notice that the condition is checked at the beginning of the loop. Then, the loop body is executed if the condition is True. If the condition is False, the loop is exited. Inside the loop, the iteration variable is updated before checking the condition again.

In the code example below, we have a while loop that prints the numbers from 1 to 5.

Listing 15.5: A while loop that prints the numbers from 1 to 5. The loop will run as long as count is less than or equal to 5. First, the iteration variable count is initialized to 1. Then, the loop body is executed while count is less than or equal to 5. The loop body prints the value of count and increments count by 1. The loop stops when count is greater than 5.
# Iteration variable
count = 1

# Stop condition (stop if evaluates to False; loop as long as
# count is lower or equal than 5)
while count <= 5:

    print("Count:", count)

    # Increment iteration variable
    count += 1
    # End of loop: Send the flow back to the condition 
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5

15.6.1 Entering the Loop Before Checking the Stop Condition

Sometimes you want to ensure that the loop runs at least once. In this case, you can use a while True loop with a break statement. In Listing 15.5, we have a standard while loop that prints the numbers from 1 to 5. In Listing 15.6, we have a while True loop that prints the number 6. Notice that count starts with 6 (i.e., the stop condition is True) but the code block within the loop is executed nevertheless. This happens because the condition is checked at the end of the loop.

graph LR
  Start((Start)) --> LoopStart[Initialize<br>iteration variable]
  LoopStart --> While[while True]
  While-->Statement1["`**Loop Body**
  Statement 1
  Statement 2
  ...
  Statement N
  _(Update iteration variable)_`"]
  Statement1--->Condition -->|True| While
  Condition{While<br>Condition} -->|False| End((End))
Figure 15.3: In a DoLoop While loop, the loop body is executed before checking the stop condition. This allows the loop to run at least once, even if the stop condition is already met.
Listing 15.6: Example of a while True loop that prints the number 6. The iteration variable count is initialized to 6. The loop body prints the value of count and increments count by 1. The loop stops when count is equal to 5. Notice that the loop body is executed at least once, even if the stop condition is already met.
count = 6

while True:
    print("Count:", count)
    count += 1
    if not count <= 5:
        break
Count: 6

Checking the condition at the end is especially useful when you need to validate user input before entering the loop. For example, in the code below, the user is prompted to enter a number between 1 and 100. The loop will keep asking for input until a valid number is entered.

def validate_input():
    while True:
        user_input = int(input("Enter a number between 1 and 100: "))
        if 1 <= user_input <= 100:
            break
    return user_input

If we did not use a while True loop, we would need to duplicate the input prompt code before the loop to ensure that the loop runs at least once:

def validate_input():
    user_input = int(input("Enter a number between 1 and 100: "))
    while not 1 <= user_input <= 100:
        user_input = int(input("Enter a number between 1 and 100: "))
    return user_input

15.7 continue Statement

Sometimes you need to skip part of the code in a loop and continue to the next iteration. The continue statement:

  • ends the current iteration in a loop, and
  • continues to the next iteration.

Using continue, you can skip code for certain iterations.

for i in range(1, 6):
    # Skipping iterations 2 and 4
    if i == 2 or i == 4:
        print("Skipping", i)
        continue

    # This part of the code is skipped if continue is called
    print("Iteration", i)
Iteration 1
Skipping 2
Iteration 3
Skipping 4
Iteration 5

Whe can also use continue in a while loop. In the example below, we skip the number 3.

count = 1

while count <= 5:
    if count == 3:
        count += 1
        continue

    print("Count:", count)
    count += 1
Count: 1
Count: 2
Count: 4
Count: 5

15.8 break Statement

Sometimes you need to exit a loop before it completes all iterations. To prematurely exit loops when a condition is met, you can use the break statement.

In Listing 15.7, we have a for loop that will break when i is equal to 5. In Listing 15.8, we have a while True loop that will break when count is greater than 5.

Listing 15.7: Example of a for loop that ends prematurely. The loop is supposed to run from 1 to 10, but it stops when i is equal to 5. The loop prints the numbers from 1 to 4 and then prints “Finished execution.”
for i in range(1, 11):
    # Premature termination condition
    if i == 5:
        break  # Break out of the loop!
    print("Iteration", i)
# 'break' makes the flow jump to the first code line after the loop
print("Finished execution.")
Iteration 1
Iteration 2
Iteration 3
Iteration 4
Finished execution.
Listing 15.8: Example of a while True loop that ends prematurely. The loop is supposed to run indefinitely, but it stops when count is greater than 5. The loop prints the numbers from 1 to 5 and then prints “Finished program.”
count = 1

while True:
    
    # Stop condition
    if count > 5:
        break

    print("Count:", count)
    count += 1

print("Finished program.")
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
Finished program.

15.9 Exercises

15.9.1 Cumulative Sum of Numbers

Write a function named cumulative_sum_numbers that takes an integer n as input and returns the sum of all numbers from 1 to n.

def cumulative_sum_numbers(n):
    pass # Add code here!

def test_cumulative_sum_numbers():
    assert cumulative_sum_numbers(5) == 15
    assert cumulative_sum_numbers(10) == 55
    assert cumulative_sum_numbers(1) == 1

15.9.2 Factorial of a Number

Write a function named factorial that takes an integer n as input and returns the factorial of n.

def factorial(n):
    pass # Add code here!

def test_factorial():
    assert factorial(5) == 120
    assert factorial(0) == 1
    assert factorial(1) == 1

15.9.3 Calculate the Power of a Number

Write a function named power that takes two numbers, x and y, as input and returns x raised to the power of y. Use a loop to calculate the power.

def power(x, y):
    pass # Add code here!

def test_power():
    assert power(2, 3) == 8
    assert power(5, 0) == 1
    assert abs(power(3, -2) - 0.111111111) < 1e-8

15.9.4 Check Prime Number

Write a function named is_prime that takes an integer n as input and returns True if n is a prime number, otherwise returns False.

def is_prime(n):
    pass # Add code here!

def test_is_prime():
    assert is_prime(7) == True
    assert is_prime(4) == False
    assert is_prime(17) == True

15.9.5 Average of Numbers

Write a function named avg_numbers that takes an integer n as input and returns the average of all numbers from 1 to n.

def avg_numbers(n):
    pass # Add code here!

def test_avg_numbers():
    assert avg_numbers(5) == 3
    assert avg_numbers(10) == 5.5
    assert avg_numbers(1) == 1

15.9.6 Sum of Odd Numbers

Write a function named sum_odd_numbers that takes an integer n as input and returns the sum of all odd numbers from 1 to n.

def sum_odd_numbers(n):
    pass # Add code here!

def test_sum_odd_numbers():
    assert sum_odd_numbers(5) == 9
    assert sum_odd_numbers(10) == 25
    assert sum_odd_numbers(1) == 1

15.9.9 Reverse a String

Write a function named reverse_string that takes a string s as input and returns a new string with the characters of s reversed.

Hint
  • Use len() to get the size of the string.
  • Use slicing or loop to reverse the string.
def reverse_string(s):
    pass # Add code here!

def test_reverse_string():
    assert reverse_string("Hello, World!") == "!dlroW ,olleH"
    assert reverse_string("") == ""
    assert reverse_string("12345") == "54321"
    assert reverse_string("a") == "a"

15.9.10 Count Vowels in a String

Write a function named count_vowels that takes a string s as input and returns the count of vowels (A, E, I, O, U) in the string.

def count_vowels(s):
    pass # Add code here!

def test_count_vowels():
    assert count_vowels("Hello, World!") == 4
    assert count_vowels("") == 0
    assert count_vowels("AEIOUaeiou") == 10
    assert count_vowels("12345") == 0

15.9.11 Remove Spaces from a String

Write a function named remove_spaces that takes a string s as input and returns a new string with all spaces removed. (Do not use string methods like replace or split.)

def remove_spaces(s):
    pass # Add code here!

def test_remove_spaces():
    assert remove_spaces("Hello, World!") == "Hello,World!"
    assert remove_spaces("") == ""
    assert remove_spaces("Remove spaces") == "Removespaces"
    assert remove_spaces("123 45") == "12345"

15.9.13 Find a Random Number Less Than 50

Write a function named find_random_number that repeatedly generates random numbers until it finds a number less than 50.

import random

def find_random_number():
    # This code will generate random sequences
    random.seed(400)  # Change this number to get new sequences

    pass # Add code here!

def test_find_random_number():
    assert find_random_number() < 50

15.9.14 Generate a Random Prime Number

Write a function named generate_random_prime that repeatedly generates random numbers until it finds a prime number. Stop when a prime number is found, and print how many iterations it took to find the prime number.

Hint

Create a function is_prime to be used inside the loop.

import random

def is_prime(n):
    pass # Add code here!

def generate_random_prime():
    # This code will generate random sequences
    random.seed(400)  # Change this number to get new sequences

    pass # Add code here!

def test_generate_random_prime():
    generate_random_prime()
    # Expected output with random seed 45:
    # Random Prime Number: 67 / #Iterations: 1

    # Expected output with random seed 7:
    # Random Prime Number: 7 / #Iterations: 2

    # Expected output with random seed 400:
    # Random Prime Number: 53 / #Iterations: 6

15.9.15 Find the Largest Digit in a Number

Write a function named largest_digit that takes an integer n as input and returns the largest digit in n.

def largest_digit(n):
    pass # Add code here!

def test_largest_digit():
    assert largest_digit(12345) == 5
    assert largest_digit(987654321) == 9
    assert largest_digit(0) == 0