Loops and Iterating

Most programs require code that runs repeatedly. Such code runs while a given condition remains truthy or until it becomes falsy (they mean the same thing). Most programming languages, including Python, use loops to provide this capability.

Python loops have several forms, but the main looping structures are the for and while statements. These loops execute a block repeatedly while a condition remains truthy. You can also think of the loop running until the condition becomes falsy. Both mental models are equivalent.

Python has three other looping mechanisms: comprehensions, generators, and functional loops. We'll talk about comprehensions in this chapter. However, we'll postpone the discussion of generators and functional loops to a later course in the Core Curriculum.

Let's begin with the while loop.

while Loops

A while loop uses the while keyword followed by a conditional expression, a colon (:), and a block. The loop repeatedly executes the block while the conditional expression remains truthy. In most programs, that loop should ultimately stop repeating. That means the block must do something to help Python know when to stop. That is, it must arrange to terminate the loop. That's usually triggered by evaluating a conditional expression. Otherwise, the loop is an infinite loop that never stops repeating.

To see why a while loop is useful, consider writing some code that prints numbers from 1 to 10:

print(1)
print(2)
print(3)
print(4)
print(5)
print(6)
print(7)
print(8)
print(9)
print(10)

While that code is straightforward and readily understood, it's easy to see why this approach is unsustainable. Suppose we need to print the numbers from 1 to 1000 or 1,000,000. If you had to write all that code for such a simple task, you would soon regret your career choice.

Let's rewrite the program with a while loop. Create a file named counter.py with the following code and run it:

counter = 1
while counter <= 10:
    print(counter)
    counter += 1

This code does the same thing as the first program but more programmatically. It iterates over the block for all integer values between 1 and 10, inclusive. Each time the block runs is called an iteration.

If you want to print 1000 numbers or even a million, all you have to change is the conditional expression:

counter = 1
# highlight
while counter <= 1000:
# endhighlight
    print(counter)
    counter += 1

Go ahead and run the code now using the command line:

python counter.py

When Python encounters this while loop, it evaluates the conditional expression, counter <= 1000. Since counter's initial value is 1, the expression is initially True, so Python executes the block. We print counter's value inside the block, then increment it by 1.

After the first iteration, Python re-evaluates the conditional expression. This time, counter is 2, which is still less than or equal to 1000; thus, the block runs again. After 1000 iterations, counter's value becomes 1001. Since the loop condition is no longer truthy, the program stops looping. It continues with the first expression or statement after the loop.

Line 4 in this example is crucial. The block must modify counter somehow, ultimately making the loop condition falsy. If it doesn't, the loop never ends, which is usually not what you want. If your program never stops running, you probably have an infinite loop somewhere in the program. Try commenting out line 4 and rerun the code. You'll see that it continually prints the number 1. You can use the Control+c keystroke to terminate the program.

Go ahead and uncomment line 4.

Using while Loops with Sequences

One of the most common uses of loops in programming is to iterate over a sequence's elements and perform some action on each element. For example, we may want to iterate over a list of names and create a new list that contains the names in uppercase. Here's one way to do that:

names = ['Chris', 'Max', 'Karis', 'Victor']
upper_names = []
index = 0

while index < len(names):
    upper_name = names[index].upper()
    upper_names.append(upper_name)
    index += 1

print(upper_names);
# ['CHRIS', 'MAX', 'KARIS', 'VICTOR']

A bit of explanation is in order here. The variable names holds a list of names. We want to append each name, in uppercase, to the initially empty upper_names list. Since list indexes are zero-based, we initialize an index variable with 0.

Next, we use a loop that executes as long as the number in index is smaller than the length of the names list. Line 8 increments the index by 1 after each iteration, which ensures that index < len(names) becomes falsy after the loop handles the last element.

Line 6 accesses the name stored at names[index] and uses it to call str.upper. That method returns the name in uppercase, which we assign to upper_name. It doesn't change the original name in the names list.

Line 7 uses list.append to append the latest uppercase name to the upper_names list. Over the four iterations of the names list, line 7 appends four uppercase names to upper_names, one per iteration, in the same order the loop processes them.

Notice that we initialized names, upper_names, and index before the loop. We don't want to initialize them inside the loop; they would get reset during every iteration. Basically, nothing would change. That wouldn't work well even if the code ran without error.

for Loops

for loops have the same purpose as while loops, but they use a condensed syntax that works well when iterating over lists and other sequences. A for loop lets you forget about indexing your sequences. You don't have to initialize or increment the index value or even need a condition. Moreover, for loops work on all built-in collections (including strings). Most loops you write in Python will be for loops.

for element in collection:
    # loop body: do something with the element

If collection is a sequence, this structure behaves in much the same way as:

index = 0
while index < len(collection):
    # loop body
    # do something with collection[index]
    index += 1

The for loop is more elegant and significantly less error-prone.

Let's rewrite names.py with a for loop to better illustrate using for:

names = ['Chris', 'Max', 'Karis', 'Victor']
upper_names = []

# highlight
for name in names:
# endhighlight
    upper_name = name.upper()
    upper_names.append(upper_name)
    # highlight
    # Deleted: index += 1
    # endhighlight

print(upper_names);
# ['CHRIS', 'MAX', 'KARIS', 'VICTOR']

The result is the same as with the while loop. However, the code is much easier to read and maintain.

For illustrative purposes, let's see what happens when we use a for loop to iterate over a string:

for char in 'Launch School':
    print(char)
L
a
u
n
c
h

S
c
h
o
o
l

If you want to work with words instead of characters, you can use the split method:

for word in 'Launch School'.split():
    print(word)
Launch
School

You can also use for loops with other collections, such as sets and dicts. Any iterable collection, in fact:

# Looping over a set
my_set = {1000, 2000, 3000, 4000, 5000}
for member in my_set:
    print(member)
# The output may not be in this sequence since it
# comes from a set.
4000
2000
5000
3000
1000
# Looping over a dictionary
my_dict = {'a': 1, 'b': 2, 'c': 3}
for key in my_dict:
    print(key)
a
b
c

Using a for loop with a dict iterates over the dict keys by default. If you want the values or pairs, you can request them with the values or items methods:

# Looping over a dictionary's values
my_dict = {'a': 1, 'b': 2, 'c': 3}
for value in my_dict.values():
    print(value)
1
2
3
# Looping over a dictionary's key/value pairs
my_dict = {'a': 1, 'b': 2, 'c': 3}
for item in my_dict.items():
    print(item)
('a', 1)
('b', 2)
('c', 3)

A more Pythonic way to iterate over both the keys and values simultaneously is to use tuple unpacking:

# Looping over a dictionary's key/value pairs
my_dict = {'a': 1, 'b': 2, 'c': 3}
for (key, value) in my_dict.items():
    print(f'{key} = {value}')
a = 1
b = 2
c = 3

We'll cover tuple unpacking in the Core Curriculum. For now, all you need to know is that each key returned by the items method gets assigned to the key variable, and each associated value gets assigned to the value variable.

By the way: you don't need the parentheses around key, value. The following code works and is more Pythonic:

# Looping over a dictionary's key/value pairs
my_dict = {'a': 1, 'b': 2, 'c': 3}
for key, value in my_dict.items():
    print(f'{key} = {value}')

Nested Loops

You will often need to nest loops within one or more outer loops. For instance, suppose you want to create a deck of cards given two lists: the suits and ranks you want to combine:

suits = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
ranks = [
    '2', '3', '4', '5', '6', '7', '8', '9', '10',
    'Jack', 'Queen', 'King', 'Ace',
]

deck = []
for suit in suits:
    for rank in ranks:
        card = f'{rank} of {suit}'
        deck.append(card)

print(deck)

With nested for loops, you start with the outer loop and assign the variable to the first element of its collection (suit and suits). You then process the inner loop in its entirety. Here, we match the first suit with every possible card rank, appending each card to the deck.

Once the inner loop finishes processing, control returns to the outer loop. Working with the second element of the outer loop's collection, it again iterates over the inner loop's collection. This continues until all members of the outer loop's collection have been processed. In our card deck example, the outer loop runs 4 times, while the inner loop runs 4 * 13 (52) times.

Nesting 3 or more levels deep is possible but frequently inadvisable. Too much nesting is hard to understand. Using one or more helper functions to handle the more deeply nested processing is often a good idea.

You can also nest while loops and mix for and while loops.

Controlling Loops

Python uses the keywords continue and break to provide more control over for and while loops. continue starts a new loop iteration; break terminates the loop early.

Continuing a Loop With Next Iteration

Let's stick with the names.py program. Suppose we want all the uppercase names in our upper_names list except 'Max'. The continue statement can help us do that.

names = ['Chris', 'Max', 'Karis', 'Victor']
upper_names = []

for name in names:
    if name == 'Max':
        continue

    upper_name = name.upper()
    upper_names.append(upper_name)

print(upper_names);
# ['CHRIS', 'KARIS', 'VICTOR']

The result doesn't contain 'MAX'.

When a loop encounters the continue keyword, it skips running the rest of the block and jumps ahead to the next iteration. In this example, we tell the loop to ignore 'Max' and skip to the next iteration without adding 'MAX' to upper_names.

You can often rewrite a loop that uses continue with a negated if conditional:

names = ['Chris', 'Max', 'Karis', 'Victor']
upper_names = []

for name in names:
    if name != 'Max':
        upper_name = name.upper()
        upper_names.append(upper_name)

print(upper_names);
# ['CHRIS', 'KARIS', 'VICTOR']

This code behaves like the version that uses continue but is a little more concise.

Why bother using continue if we can write looping logic without it? You don't have to use continue, of course. However, it often leads to a more elegant solution to a problem. Without continue, your loops can get cluttered with nested conditional logic.

for value in collection:
    if some_condition():
        # some code here
        if another_condition():
            # some more code here

We can use continue to rewrite this loop without the nested ifs:

for value in collection:
    if not some_condition():
        continue

    # some code here

    if not another_condition():
        continue

    # some more code here

Which of these is more readable and easier to maintain? That's not always easy to answer. As is often the case, the choice may be made for you by local coding standards. At other times, it's a matter of deciding which version looks and reads better.

The continue statement tells Python to start the next iteration of the nearest enclosing loop. You can't start a new iteration of an outer loop if you're currently in an inner (nested) loop.

Breaking Out of a Loop

Instead of exiting the current iteration, you sometimes want to stop iterating completely. For instance, when you search a list for a specific value, you probably want to stop searching once you find it. There's no reason to keep searching if you don't need any subsequent matches.

Let's explore this idea with some code. Create a file named search.py with the following code and run it from your terminal:

numbers = [3, 1, 5, 9, 2, 6, 4, 7]
found_item = -1
index = 0

while index < len(numbers):
    if numbers[index] == 5:
        found_item = index

    index += 1

print(found_item)

This program iterates over the elements of a list to find the element whose value is 5. It saves the index value in found_item. However, the loop continues to iterate after finding the desired value. That seems pointless and wasteful. It's where break steps in and saves the day:

numbers = [3, 1, 5, 9, 2, 6, 4, 7]
found_item = -1
index = 0

while index < len(numbers):
    if numbers[index] == 5:
        found_item = index
        # highlight
        break
        # endhighlight

    index += 1

print(found_item)

The break statement tells Python to terminate the nearest enclosing loop once we find the desired element. You can't break out of an outer loop if you're currently in an inner (nested) loop.

Emulating Do/While Loops

A typical while loop looks something like this:

while some condition is truthy
    do some work

In most code, this is a perfectly fine solution; you usually want to skip over the "do some work" part if "some condition" is initially falsy. However, sometimes you want to "do some work" at least once, even if the condition is initially falsy. For that, you need something like this:

do some work
while some condition is truthy

In other words, you always want to "do some work" at least once. This is often called a do/while or do/until loop since many languages have a looping structure with those names. Python does not, so you must take a different approach. That typically uses a break statement at the end of the loop, like so:

do some work
    if some condition is falsy
        break

This often comes up in interactive programs where you may want to execute the main program loop at least once before exiting. The code to handle such a loop might look something like this:

keep_going = True
while keep_going:
    # main loop code is here

    answer = input('Play again? (y/n) ')
    if answer == 'n':
        keep_going = False

That's workable, albeit a little awkward. A slightly less clumsy alternative uses break and while True.

while True:
    # main loop code is here

    answer = input('Play again? (y/n) ')
    if answer == 'n':
        break

Simultaneous Iteration

We sometimes need to iterate through multiple collections in parallel. That is, we want to grab data from several collections during each loop iteration. If all the collections are indexed sequences, you can write a while loop using indexes. However, that's error-prone and often messy.

Meet the hero of parallel iteration: zip! The zip function is specifically designed to make simultaneous iteration easy. Let's see what this looks like.

Suppose you need to print the full names of several thousand people. For bizarre historical reasons, you have two lists: one contains the forenames, and the other contains the corresponding surnames. Without zip, you need to use a while loop and indexing. That's feasible, so let's try it. We'll use 4 names to test the concept:

forenames = ['Ken', 'Lynn', 'Pat', 'Nancy']
surnames = ['Camp', 'Blake', 'Flanagan', 'Short']

index = 0
while index < len(forenames):
    if index >= len(surnames): # surnames might be shorter.
        break

    forename = forenames[index]
    surname = surnames[index]
    print(f'{forename} {surname}')

    index += 1
Ken Camp
Lynn Blake
Pat Flanagan
Nancy Short

While it's not horrid, it's a lot of work. We can do better than that with zip:

forenames = ['Ken', 'Lynn', 'Pat', 'Nancy']
surnames = ['Camp', 'Blake', 'Flanagan', 'Short']

zipped_names = zip(forenames, surnames)
for forename, surname in zipped_names:
    print(f'{forename} {surname}')

That definitely looks better! However, we should explain lines 4 and 5. On line 4, zip creates a lazy sequence that acts like a list of tuples. Each tuple contains a forename and a surname. Line 5 is the start of our loop. We're taking advantage of the fact that for can assign multiple variables when the collection elements are tuples. Thus, we can grab the forename and surname from the current tuple.

Note that zip takes care of the potential problem of dealing with lists of different sizes. Our first attempt needed that if statement in the while block to guard against the surnames list being shorter than forenames.

Comprehensions

Python supports a concise and readable way to create mutable collections from existing iterable collections: comprehensions. There are 3 comprehension types: list, dict, and set. Properly used, comprehensions can simplify your code and make it easier to understand.

Unlike most for and while loops, which are statements, comprehensions are expressions. You can use a comprehension on the right side of an assignment, as a function argument, as a return value, or any other place where you can use an expression that evaluates as a list, dict, or set. You can even use them as standalone expressions:

[print(foo) for foo in collection]

However, an ordinary for loop is the preferred alternative.

List Comprehensions

The most commonly used comprehensions are list comprehensions. They take an iterable collection and create a new list through iteration and optional selection. List comprehensions have the following format:

[ expression for element in iterable if condition ]

The if condition portion is optional: it tells Python to select only certain elements from the iterable. The for element in iterable portion describes the iteration: it looks exactly like a for loop. It can be read in much the same way. Finally, the expression is a value that gets returned by each iteration of the loop. Python collects all the return values and puts them in a new list.

The expression in a comprehension often performs a transformation. It determines a new value based on an element from the original collection. Such comprehensions are called transformations.

If the if condition portion is present, we say that the comprehension also performs selection. With selections, it's not uncommon to return the original values from the collection:

[ element for element in iterable if condition ]

The terms transformation and selection are handy to remember. You will encounter them in many languages.

Consider this basic example of a transformative list comprehension:

squares = [ number * number for number in range(5) ]
print(squares)      # [0, 1, 4, 9, 16]

Here, we're iterating over the numbers in the indicated range: 0, 1, 2, 3, 4. We compute the square with number * number for each number. Finally, Python collects all the squares into a list and assigns the list to the squares variable. Voila! We now have a list of squares.

Of course, we could do the same thing with an ordinary for loop:

squares = []
for number in range(5):
    square = number * number
    squares.append(square)

print(squares)      # [0, 1, 4, 9, 16]

That's quite a bit more code, and the effort of reading it is a bit higher.

Let's look at a selection example:

multiples_of_6 = [ number for number in range(20)
                   if number % 6 == 0 ]
print(multiples_of_6)      # [0, 6, 12, 18]

Here, we see the pattern of using the original collection values as the expression. We've also split the comprehension over two lines, which can aid readability.

This example combines selection and transformation:

even_squares = [ number * number
                 for number in range(10)
                 if number % 2 == 0 ]
print(even_squares)      # [0, 4, 16, 36, 64]

This code selects all of the even numbers in the specified range and then returns a list of the squares of the chosen numbers.

Let's try iterating over a dictionary. Suppose we have a dict of cats. We're using the cat names as keys; each name is associated with the cat's coat color. We want to create a list of the names in uppercase:

cats_colors = {
    'Tess':   'brown',
    'Leo':    'orange',
    'Fluffy': 'gray',
    'Ben':    'black',
    'Kat':    'orange',
}

names = [ name.upper() for name in cats_colors ]
print(names)
# ['TESS', 'LEO', 'FLUFFY', 'BEN', 'KAT']

This is nearly identical to using the dict.keys method. The only difference is that the list comprehension returns an ordinary list, not a dictionary view object.

You can also iterate over the values by iterating in cats_colors.values() or the key/value pairs by iterating in cats_colors.items().

Suppose we now want to limit the result list to just orange cats. We can accomplish that by adding a selection criterion to the comprehension:

cats_colors = {
    'Tess':   'brown',
    'Leo':    'orange',
    'Fluffy': 'gray',
    'Ben':    'black',
    'Kat':    'orange',
}

names = [ name.upper()
          for name in cats_colors
          if cats_colors[name] == 'orange' ]
print(names) # ['LEO', 'KAT']

It's also possible to use multiple selection criteria. Let's limit the result to cats whose name begins with L:

cats_colors = {
    'Tess':   'brown',
    'Leo':    'orange',
    'Fluffy': 'gray',
    'Ben':    'black',
    'Kat':    'orange',
}

names = [ name.upper()
          for name in cats_colors
          if cats_colors[name] == 'orange'
          if name[0] == 'L' ]
print(names) # ['LEO']

Multiple selection criteria act like nested if statements or as and-ed conditions. The selections combine, so only collection members matching all criteria are selected.

Comprehensions can also have multiple for loop components. For instance, let's generate a deck of cards based on a list of the suits and a list of the ranks:

suits = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
ranks = [
    '2', '3', '4', '5', '6', '7', '8', '9', '10',
    'Jack', 'Queen', 'King', 'Ace',
]

deck = [ f'{rank} of {suit}'
         for suit in suits
         for rank in ranks ]
print(deck)

Compare this code with the one shown earlier in Nested Loops. As multiple selection criteria resemble a nested if, multiple looping components resemble a nested for loop.

Dictionary Comprehensions

Dictionary comprehensions are almost identical to list comprehensions. However, they create new dictionaries instead of lists. Syntactically, they use curly braces instead of square brackets, and the expression becomes a "key: value" pair. Dictionary comprehensions have the following format:

{ key: value for element in iterable if condition }

The most significant difference is that the expression component is now a key/value pair, each given by another expression.

Consider this basic example of a dictionary comprehension:

squares = { f'{number}-squared': number * number
            for number in range(1, 6) }
print(squares)
# pretty-printed for clarity.
{
    '1-squared': 1,
    '2-squared': 4,
    '3-squared': 9,
    '4-squared': 16,
    '5-squared': 25
}

Other than the differences mentioned above, list and dict comprehensions are identical.

Set Comprehensions

Set comprehensions look almost identical to dict comprehensions. However, they create a new set instead of a dict and only have one expression to the left of the word for:

{ expression for element in iterable if condition }

Previously, a colon-separated key/value expression pair was to the left of for. Now, we only have an expression.

Consider this basic example of a set comprehension:

squares = { number * number for number in range(1, 6) }
print(squares)      # {1, 4, 9, 16, 25}

Other than the differences mentioned above, dict and set comprehensions are identical.

Why No Tuple, Range, or String Comprehensions?

Python programmers often wonder why Python doesn't have tuple comprehensions. They can easily see that the following valid code hints that it might be a tuple comprehension:

squares = ( number * number for number in range(1, 6) )
print(squares) # <generator object <genexpr> at 0x104e39a40>

However, it is not a comprehension. Instead, it is a generator expression. We won't discuss generators in this book, but you'll see them later in the Core Curriculum. You'll also learn why they are helpful. The fact that a generator expression looks like it should be tuple comprehension is merely a matter of syntax. It doesn't answer the question of why.

Comprehensions don't build their results all at once. Each kind of comprehension works something like this:

result = empty_collection               # [], {}, set()
for item in collection:
    result.append(item)

As you can see, our result starts as an empty collection. We then modify the result collection during each iteration by appending a new item to result. From this, it's clear that the result must be a mutable type. Tuples are immutable, so Python can't have tuple comprehensions.

Since ranges and strings are also immutable, comprehensions can't create them. If you must have a tuple or string, use the tuple or str constructors to convert a list comprehension's result into a tuple or string. (Sorry, but you can't do something similar to create a range.)

Summary

Loops and comprehensions are a great way to perform repeated operations on a collection. In Python, you'll often find yourself reaching for a comprehension before a loop, but not always. Let's test these concepts with some exercises!

Exercises

  1. The following code causes an infinite loop (a loop that never stops iterating). Why?

    counter = 0
    
    while counter < 5:
        print(counter)
    

    Solution

    The problem occurs in the loop body. We never increment counter, so counter < 5 always returns a truthy value.

    Video Walkthrough

    Please register to play this video

  2. Modify the age.py program you wrote in Exercise 3 of the Input/Output chapter. The updated code should use a for loop to display the future ages.

    Solution

    age = int(input('How old are you? '))
    print(f'You are {age} years old.')
    print()
    
    for future in range(10, 50, 10):
        print(f'In {future} years, you will be '
              f'{age + future} years old.')
    

    Video Walkthrough

    Please register to play this video

  3. Use a while loop to print the numbers in my_list, one number per line. Then, do the same with a for loop.

    my_list = [6, 3, 0, 11, 20, 4, 17]
    
    6
    3
    0
    11
    20
    4
    17
    

    Solution

    my_list = [6, 3, 0, 11, 20, 4, 17]
    
    index = 0
    while index < len(my_list):
        number = my_list[index]
        print(number)
        index += 1
    
    my_list = [6, 3, 0, 11, 20, 4, 17]
    
    for number in my_list:
        print(number)
    

    Our solution using a while loop uses indexing to control iteration and to access the list members. Note that we start by setting index to 0 and then iterate while index is less than the list length.

    The solution using a for loop is clearly easier to understand -- we don't have to mess around with indexing; we only need to iterate over the list elements.

    Video Walkthrough

    Please register to play this video

  4. Use a while loop to print all numbers in my_list with even values, one number per line. Then, print the odd numbers using a ' for' loop.

    my_list = [6, 3, 0, 11, 20, 4, 17]
    
    6
    0
    20
    4
    
    3
    11
    17
    

    Solution

    my_list = [6, 3, 0, 11, 20, 4, 17]
    
    index = 0
    while index < len(my_list):
        number = my_list[index]
        # Even numbers are exactly divisible by 2
        if number % 2 == 0:
            print(number)
    
        index += 1
    
    my_list = [6, 3, 0, 11, 20, 4, 17]
    
    for number in my_list:
        # Odd numbers are not exactly divisible by 2
        if number % 2 != 0:
            print(number)
    

    Our solutions both rely on using the % operator to determine whether a number is exactly divisible by 2. Even numbers are; odd numbers aren't. In both cases, we only need to compare the result of element % 2 with 0. If number % 2 is 0, the number is even. Otherwise, it is odd.

    As with the previous problem, we needed indexing to control the while loop. However, the for loop led to code that is easier to read.

    Video Walkthrough

    Please register to play this video

  5. Print all of the even numbers in the following list of nested lists. Don't use any while loops.

    my_list = [
        [1, 3, 6, 11],
        [4, 2, 4],
        [9, 17, 16, 0],
    ]
    
    6
    4
    2
    4
    16
    0
    

    Solution

    my_list = [
        [1, 3, 6, 11],
        [4, 2, 4],
        [9, 17, 16, 0],
    ]
    
    for nested_list in my_list:
        for number in nested_list:
            if number % 2 == 0:
                print(number)
    

    That may not have been too easy. Nested loops are hard to think about, but you'll encounter them often enough to have dreams about them. We start by iterating over the nested lists inside my_list. That means nested_list takes on the values [1, 3, 6, 11], [4, 2, 4], and [9, 7, 16, 0] as the iteration proceeds. We then iterate over the numbers in the current nested list during each iteration. Finally, we print the even numbers.

    Video Walkthrough

    Please register to play this video

  6. Let's try another variation on the even/odd-numbers theme.

    We'll return to the simpler one-dimensional version of my_list. In this problem, you should write code that creates a new list with one element for each number in my_list. If the original number is an even, then the corresponding element in the new list should contain the string 'even'; otherwise, the element should contain 'odd'.

    my_list = [
        1, 3, 6, 11,
        4, 2, 4, 9,
        17, 16, 0,
    ]
    
    # pretty-printed for clarity
    [
        'odd', 'odd', 'even', 'odd',
        'even', 'even', 'even', 'odd',
        'odd', 'even', 'even'
    ]
    

    Solution

    my_list = [
        1, 3, 6, 11,
        4, 2, 4, 9,
        17, 16, 0,
    ]
    
    result = []
    for number in my_list:
        if number % 2 == 0:
            result.append('even')
        else:
            result.append('odd')
    
    print(result)
    

    Our approach is straightforward: we iterate over all the numbers in the list and check whether each is even. Based on the result, we append either 'even' or 'odd' to the result list.

    You may have struggled if you tried to use a list comprehension for this problem. Since comprehensions don't have an else capability, trying to generate 'even' for some values and 'odd' for others is challenging. You can use a ternary expression in the comprehension, but this is a little confusing visually:

    my_list = [
        1, 3, 6, 11,
        4, 2, 4, 9,
        17, 16, 0,
    ]
    
    #highlight
    result = [ 'even' if number % 2 == 0 else 'odd'
               for number in my_list ]
    #endhighlight
    print(result)
    

    On line 7, we've used a ternary expression to choose between the two values. The ternary is equivalent to:

    if number % 2 == 0:
        return 'even'
    else:
        return 'odd'
    

    A cleaner approach is to use a helper function to determine whether we should add 'even' or 'odd' to the new list:

    my_list = [
        1, 3, 6, 11,
        4, 2, 4, 9,
        17, 16, 0,
    ]
    
    #highlight
    def odd_or_even(number):
        return 'even' if number % 2 == 0 else 'odd'
    
    result = [ odd_or_even(number)
    #endhighlight
               for number in my_list ]
    print(result)
    

    Video Walkthrough

    Please register to play this video

  7. Write a find_integers function that returns a list of all the integers from my_tuple:

    my_tuple = (1, 'a', '1', 3, [7], 3.1415,
                -4, None, {1, 2, 3}, False)
    integers = find_integers(my_tuple)
    print(integers)                    # [1, 3, -4]
    

    You can use the expression type(object) is int to determine whether an object is an integer. For instance:

    print(type(True) is int)      # False (boolean)
    print(type([1, 2, 3]) is int) # False (list)
    print(type(3.141592) is int)  # False (float)
    print(type(77) is int)        # True
    

    You may receive a SyntaxWarning warning message from the last two examples. You can ignore that warning.

    Solution

    def find_integers(things):
        return [ element
                 for element in things
                 if type(element) is int ]
    
    my_tuple = (1, 'a', '1', 3, [7], 3.1415,
                -4, None, {1, 2, 3}, False)
    integers = find_integers(my_tuple)
    print(integers)                    # [1, 3, -4]
    

    Our solution uses a list comprehension to iterate through the elements in the things argument and create a new list.

    It's worth noting that we used a list comprehension to iterate over the tuple. The main reason for that choice is that find_integers is expected to return a list, not a tuple. However, an even more important reason is that there is no such thing as a "tuple comprehension". Comprehensions don't care what kind of collection you're iterating, but the result must always be a list, set, or dictionary.

    Video Walkthrough

    Please register to play this video

  8. Write a comprehension that creates a dict object whose keys are strings and whose values are the length of the corresponding key. Only keys with odd lengths should be in the dict. Use the set given by my_set as the source of strings.

    my_set = {
        'Fluffy',
        'Butterscotch',
        'Pudding',
        'Cheddar',
        'Cocoa',
    }
    

    Solution

    my_set = {
        'Fluffy',
        'Butterscotch',
        'Pudding',
        'Cheddar',
        'Cocoa',
    }
    
    result = { name: len(name)
               for name in my_set
               if len(name) % 2 != 0 }
    print(result)
    # {'Cheddar': 7, 'Pudding': 7, 'Cocoa': 5}
    

    Remember: sets are unordered, so your result may have a different ordering.

    Video Walkthrough

    Please register to play this video

  9. Don't let the math scare you. This is a logic and syntax problem, not a math problem.

    Write a function that computes and returns the factorial of a number by using a for or while loop. The factorial of a positive integer n, signified by n!, is defined as the product of all integers between 1 and n, inclusive:

    n! Expansion Result
    1! 1 1
    2! 1 * 2 2
    3! 1 * 2 * 3 6
    4! 1 * 2 * 3 * 4 24
    5! 1 * 2 * 3 * 4 * 5 120

    You may assume that the argument is always a positive integer.

    print(factorial(1))   # 1
    print(factorial(2))   # 2
    print(factorial(3))   # 6
    print(factorial(4))   # 24
    print(factorial(5))   # 120
    print(factorial(6))   # 720
    print(factorial(7))   # 5040
    print(factorial(8))   # 40320
    print(factorial(25))  # 15511210043330985984000000
    

    Solution

    def factorial(n):
        result = 1
        while n > 0:
            result *= n
            n -= 1
    
        return result
    
    def factorial(n):
        result = 1
        for number in range(n, 0, -1):
            result *= number
    
        return result
    

    Our first solution uses a while loop to compute the return value. We begin by assigning the result variable to 1, then we multiply result by all of the integers between n and 1, inclusive. Note that we need to decrement n by 1 at the end of each iteration.

    The second solution is similar, but it uses a for loop instead of a while loop. The benefit of using for is that we don't need to worry about decrementing n. Instead, we just iterate over the integers between n and 1, inclusive, using a range.

    Video Walkthrough

    Please register to play this video

  10. The following code uses the randrange function from Python's random library to obtain and print a random integer within a given range. Using a while loop, it keeps running until it finds a random number that matches the last number in the range. Refactor the code so it doesn't require two different invocations of randrange.

    import random
    
    highest = 10
    number = random.randrange(highest + 1)
    print(number)
    
    while number != highest:
        number = random.randrange(highest + 1)
        print(number)
    

    Solution

    import random
    
    highest = 10
    while True:
        number = random.randrange(highest + 1)
        print(number)
        if number == highest:
            break
    

    The ideal do/while loop use case occurs when you need to execute some code at least once. We need to do that here. However, Python doesn't have a do/while loop. Instead, we can combine a while True loop with a break statement.

    Video Walkthrough

    Please register to play this video

  11. Challenging Problem: Don't feel bad if you struggle with this problem or can't solve it. The problem is not easy. It is designed to demonstrate why we prefer to use for loops when we can, and a big part of that problem is messy code that is difficult to write and understand. See how far you can get, but don't spend much time struggling.

    Print all of the even numbers in the following list of nested lists. This time, don't use any for loops.

    my_list = [
      [1, 3, 6, 11],
      [4, 2, 4],
      [9, 17, 16, 0],
    ]
    
    6
    4
    2
    4
    16
    0
    

    Solution

    my_list = [
      [1, 3, 6, 11],
      [4, 2, 4],
      [9, 17, 16, 0],
    ]
    
    outer_index = 0
    while outer_index < len(my_list):
        inner_index = 0
        while inner_index < len(my_list[outer_index]):
            number = my_list[outer_index][inner_index]
            if number % 2 == 0:
                print(number)
    
            inner_index += 1
    
        outer_index += 1
    

    Phew! That's messy and hard to follow, isn't it? While the individual loops aren't tough to understand, keeping track of two different indexes and getting them to work together is no mean feat. We also have to use both indexes on lines 10 and 11.

    We can simplify this solution a bit by assigning each nested list to a local variable:

    my_list = [
      [1, 3, 6, 11],
      [4, 2, 4],
      [9, 17, 16, 0],
    ]
    
    outer_index = 0
    while outer_index < len(my_list):
        inner_list = my_list[outer_index]
        inner_index = 0
        while inner_index < len(inner_list):
            number = inner_list[inner_index]
            if number % 2 == 0:
                print(number)
    
            inner_index += 1
    
        outer_index += 1
    

    Video Walkthrough

    Please register to play this video