Loop structures allow the programmer to dynamically iterate or repeat code. We have already seen the for loop, which allows us to loop over sequential data, like the characters in a string, values in a list, or numbers in a range. In this chapter, we will learn about the while loop, which allows us to write loops controlled by a boolean condition.
The semantics of a while loop is the same across most programming languages, though the syntax will differ by language. Here is the syntax for Python:
while CONDITION:
# any code indented one level executes if CONDITION evaluates to True
# at the end of this block, program control returns to test the CONDITION again
# If it is True again, the block of code inside the while loop executes again.
# The loop continues in this way until the CONDITION evaluates to False, at
# which point control flows to the statement that follows the while loop.
Let’s see this in action! The following code will count down the numbers from 10 down to 1.
# use a variable to keep track of the current number
# start at 10
currentNumber = 10
# as long as we aren't at 0
while currentNumber > 0:
# print the number
print( currentNumber )
# update the number
currentNumber = currentNumber - 1
print ("Blast off!")
What happens if we forget to update the number? We end up with an “infinite loop,” since the condition will never be False! An infinite loop is exactly what it sounds like, the loop never ends, at least not until the program crashes, or the user forces the program to quit! (This is BAD!)
Follow along with this code:
def sumDigits( n ):
""" assume n is positive and sum the digits and return the total"""
# pull off the digits and add them to a running total
# I need to initialize a running total and digitsRemaining
runningTotal = 0
digitsRemaining = n
# STOP when I have no digits left
# CONTINUE as long as I have digits left
while digitsRemaining > 0:
# pull of a digit, add it to the running total
currentDigit = digitsRemaining % 10
runningTotal = runningTotal + currentDigit
# update the digitsRemaining to remove that digit I just pulled off
digitsRemaining = digitsRemaining // 10
return runningTotal
print( sumDigits( 691 ) )
print( sumDigits( 152342 ) )
When you want to write a while loop, there are 5 questions that you should ask yourself:
What should the loop as a whole do?
When should the loop repeat?
To achieve that purpose, what should the loop do during each iteration?
What should you update inside the loop to make sure it eventually stops?
What should you initialize before the loop begins?
Let’s look at an example. Suppose that we want to have a user enter a password and we do not want to let the program continue until the user enters the correct password.
We will build up our code by writing out the answers to these questions as comments.
What should the loop as a whole do?
# Require the user to enter the correct password.
When should the loop repeat?
# While the user has not entered the correct password.
while enteredPassword != correctPassword :
To achieve that purpose, what should the loop do during each iteration?
# Ask the user for their password
enteredPassword = input ("Enter your password: ")
What should you update inside the loop to make sure it eventually stops?
We should update the enteredPassword which is used as the condition of the loop. Note that we already did this in step 3 when we asked the user to re-enter their password, but this question often requires its own answer.
What should you initialize before the loop begins?
We ended up using two identifiers during our loop: enteredPassword and correctPassword must both be initialized. Let’s assume this is inside a function and correctPassword is a parameter to the function. We can initialize enteredPassword to be an empty string before the loop.
Here is the complete program. Notice, however, how this loop will not end until the user enters the correct password. If they don’t remember the password, we have an infinite loop! Yikes! What do you think you might do differently to get the loop to stop in that case?
def checkPassword (correctPassword) :
'''Require the user to enter the correct password.'''
enteredPassword:str = ""
# Check the password entered by the user. Loop until
# they get the password right.
while enteredPassword != correctPassword:
# Ask the user for their password.
enteredPassword = input( "Enter your password: " )
# Getting to this line means the while loop condition
# just evaluated to False, i.e., enteredPassword must
# be the same as correctPassword
print( "Passwords match!" )
checkPassword("abc")
Each time that we learn a new construct, we should think about how we would test code that uses that construct. When testing a while loop, what we typically want to test is how the loop works when it iterates a different number of times. This will require that we provide different data for the while loop to operate on.
Let’s write a function called reverseNumber that is passed a number and returns the number with its digits in the reverse order. For example, if the parameter is 1234, the return value is 4321.
def reverseNumber( num:int ) -> int:
'''Reverses the digits in num and returns the reversed value.'''
# the loop variable will hold remaining digits, initialized to the original num
remainingDigits:int = num
# accumulate the reversed digits into a number
reversed:int = 0
# Reverse the digits remaining
while remainingDigits != 0:
# Determine the next digit to add
nextDigit:int = remainingDigits % 10
# Add the next digit
reversed = (10*reversed) + nextDigit
# Remove the digit from the original number
remainingDigits = remainingDigits // 10
return reversed
The number that is returned depends upon the parameter that is passed to the function. How can we be sure it will work no matter what values are passed in?
We should generally begin our testing with parameters that we think are completely “typical” and do not require any special handling. It also is a good idea to pick a test case where we can easily know what the correct answer is. We might start with something like this.
# Test a "typical" multi-digit number
actual:int = reverseNumber (1234)
print( f"Expected: 4321, Actual: {actual}")
Once we get the code to pass that test case, we should consider some special cases. With while-loops, one special case to consider is whether the code works if the while-loop body is executed exactly one time. We will need to think about what data would result in exactly one execution. In this case, that would happen if the parameter contained a single digit.
# Test a single-digit number
actual = reverseNumber( 3 )
print( f"Expected: 3, Actual: {actual}")
Once that works, another good test case is to execute the while loop with data where the while loop body is never executed. In this program, the condition controlling the while loop is False when the parameter num is 0. Let’s try that.
# Test 0 iterations
actual = reverseNumber( 0 )
print( f"Expected: 0, Actual: {actual}")
It's always good to take a step back and think about what you might have missed. In this example, we are working with integer values. What are some different categories of values?
positive numbers (we did think about this)
0 (we thought about this)
1 (we thought about this)
-1 (hmm)
negative numbers (HMM)
We might try our code on a negative number, starting with a "typical" value (multiple digits) in this case. In doing so, we have to think about the expected output.
When we run it, though, we get an infinite loop!
# Test an unexpected input, a negative number
actual = reverseNumber( -14 )
print( f"Expected: -41, Actual: {actual}")
How do we fix this? We have a few options, listed here in order of easiest (but least rigorous) to most involved.
Put a comment that makes it clear we only want positive numbers.
'''Reverses the digits in num and returns the reversed value.
Assumes num is positive.'''
Add a conditional to reject unexpected inputs.
When a return type is None, we can use an "empty" return statement to halt the function's execution.
def reverseNumber( num:int ) -> int:
'''Reverses the digits in num and returns the reversed value.
Assumes num is positive.'''
# reject unexpected inputs
if num < 0:
return
...
Update the implementation to handle these inputs.
def reverseNumber( num:int ) -> int:
'''Reverses the digits in num and returns the reversed value.
Assumes num is positive.'''
# the loop variable will hold remaining digits, initialized to the original num
remainingDigits:int = num
# if we have a negative number
if num < 0:
remainingDigits = -remainingDigits # make it positive
# accumulate the reversed digits into a number
reversed:int = 0
# Reverse the digits in num
while remainingDigits != 0:
# Determine the next digit to add
nextDigit:int = remainingDigits % 10
# Add the next digit
reversed:int = (10*reversed) + nextDigit
# Remove the digit from the original number
remainingDigits = remainingDigits // 10
# if we had a negative number
if num < 0:
reversed = -reversed # make the result negative again
return reversed
# Typical test case
actual:int = reverseNumber (1234)
print( f"Expected: 4321, Actual: {actual}")
# Test a single-digit number
actual = reverseNumber( 3 )
print( f"Expected: 3, Actual: {actual}")
# Test 0 iterations
actual = reverseNumber( 0 )
print( f"Expected: 0, Actual: {actual}")
# Test an unexpected input, a negative number
actual = reverseNumber( -14 )
print( f"Expected: -41, Actual: {actual}")
You may be wondering how to choose between a for loop and a while loop. Almost always, either construct will work and there is no clear “winner.” However, a general rule of thumb is to try to use a for loop when possible, as there is a much lower chance that you’ll introduce a bug that causes an infinite loop. If you know the number of times a loop should execute or you know that you are going to “walk over” sequential data stored in a string or list, you can use a for loop. If you don’t know a priori how many times a loop should execute, then the while loop is more appropriate. With the while loop, you must ensure that you’re moving towards the test condition becoming False to avoid an infinite loop.