Introduction to Python Programming. Section 9. The ’For’ Loop

9 The ’For’ Loop

9.1 Objectives

This section summarizes various aspects of using the for loop, ranging from trivial to more advanced. The for loop was already mentioned several times in this text, and we will give references to those subsections where relevant. Depending on what you already know, in this section you will either review or learn the following:

  • How to parse text strings one character at a time.
  • How to split a text string into a list of words.
  • How to parse tuples and lists one item at a time.
  • More about the built-in function range.
  • About using the keyword else with a for loop.
  • About list comprehension – a special form of the for loop.
  • About iterators, and how to make them.

9.2 Why learn about the for loop?

The for loop is a fundamental construct that all imperative programming languages (Python, C, C++, Fortran, Java, ...) use to iterate over sequences. These sequences can have many different forms:

  • Numbers such as 0, 1, 2, ...,
  • Text characters in a word.
  • Words in a text document.
  • Lines, words, or text characters in a text file.
  • Entries in a relational database,
  • etc.

The for loop is not present in all programming languages though. For example, pure functional programming languages such as Dylan, Erlang, Haskell or Lisp iterate over sequences using recursion instead.

In some imperative languages such as C, C++, Fortran or Java the for loop is sometimes called the counting loop, because its primarily use is to iterate over sequences of numbers.

As usual, Python is very flexible – the for loop can naturally iterate over sequences of numbers, text characters, list / tuple / dictionary items, and over even more general sequences. We will talk about all this in he following subsections.

9.3 Parsing text strings

If you do not know the for loop yet, please read Subsections 4.22 and 4.23 now. In Subsection 4.22 you will see how the keyword for is used to create a for loop, and how the for loop can be used to parse text strings one character at a time. In Subsection 4.23 you will see how the for loop can be used to reverse text strings.

9.4 Splitting a text string into a list of words

In order to parse a text document (text string) one word at a time, it is convenient to split it into a list of individual words first. This was explained in detail in Subsection 4.41. In practise "the devil is in the detail" though, and therefore in Subsection 4.42 we explained in more detail how to take care of punctuation while splitting text strings. Last, lists and list operations were explained in Section 7.

9.5 Parsing lists and tuples (including comprehension)

Lists and tuples can be parsed with the for loop one item at a time analogously to how text strings are parsed one character at a time. This was explained in detail in Subsection 7.10. The for loop works in the same way with tuples – it does not distinguish between lists and tuples at all.

Importantly, list comprehension (which also applies to tuples) is just another form of the for loop. If you do not know how to use list comprehension yet, read Subsections 7.297.31 now.

9.6 Parsing two lists simultaneously

Sometimes one needs to go over two (or more) lists simultaneously. In this case the standard approach is to use the built-in function zip which you already know from Subsection 7.28. As an example, imagine that you have two lists of numbers, and you need to create a list of their pairs, but only accept pairs where the values are in increasing order. Here is the code:

  A = [1, 6, 3, 9]
  B = [3, 2, 8, 5]
  result = []
  for x, y in zip(A, B):
      if x < y:
          result.append([x, y])
  print(result)

  [[1, 3], [3, 8]]

9.7 Iterating over sequences of numbers and the built-in function range

The built-in function range was first mentioned in Subsection 6.14. What we have not shown there though, is that range(N) creates a sequence of integers 0, 1, 2, ..., N-1. The result is an object of type range,

  s = range(5)
  print(s)
  isinstance(s, range)

  range(5)
  True

If needed, it can be cast to a list (or tuple) for display purposes:

  list(range(5))

  [0, 1, 2, 3, 4]

A for loop of the form

  for n in range(N):

goes over the corresponding sequence of numbers one number at a time:

  for i in range(5):
      print(i)

0  
1  
2  
3  
4

If needed, one can start the sequence with a nonzero integer:

  list(range(10, 15))

  [10, 11, 12, 13, 14]

It is possible to skip over numbers using an optional step argument. For example, the sequence of all odd numbers between 1 and 11 can be created as follows:

  list(range(1, 12, 2))

  [1, 3, 5, 7, 9, 11]

One has to be a bit careful when creating multiples though. Typing range(1, 16, 3) might appear to be the way to create the sequence of all multiples of 3 between 1 and 15, but that’s not the case:

  list(range(1, 16, 3))

  [1, 4, 7, 10, 13]

A better way to achieve this goal is through list comprehension:

  [1] + [3*n for n in range(1, 6)]

  [1, 3, 6, 9, 12, 15]

And last, one can create descending sequences using negative step values. This is the sequence of even numbers 20, 18, ..., 2:

  list(range(20, 0, -2))

  [20, 18, 16, 14, 12, 10, 8, 6, 4, 2]

9.8 Parsing dictionaries (including comprehension)

Recall from Subsection 7.40 that a dictionary, in fact, is just a list of tuples of the form (k, v) where k is a key and v the corresponding value. From the same subsection you also know that the list of all keys in a dictionary named D can be extracted via D.keys(), the list of all values via D.values() and the list of all items via D.items(). The latter is a list of two-item tuples.

Hence, parsing a dictionary in fact means to parse a list. One has various options. To go through the list of all keys, type:

  for k in D.keys():

The list of all values can be parsed via:

  for v in D.values():

And to go through the list of all items, type:

  for k, v in D.items():

or

  for (k, v) in D.items():

Importantly, you should make yourself familiar with how comprehension is used for dictionaries. Unless you already did, make sure to read Subsection 7.45 where we showed how to use comprehension to find all keys for a given value, as well as Subsection 7.46 where we explained how to reverse a dictionary using comprehension.

9.9 Nested for loops

Loops can be nested, meaning that the body of a loop can contain another loop (whose body can contain another loop...). In this case the former is called outer loop and the latter is the inner loop. With each new nested level the indentation increases. This is shown in the following example which creates all combinations of given numbers and letters:

  nums = [’1’, ’2’, ’3’, ’4’]
  chars = [’A’, ’B’, ’C’]
  combinations = []
  for n in nums:
      for c in chars:
          combinations.append(n + c)
  print(combinations)

  [’1A’, ’1B’, ’1C’, ’2A’, ’2B’, ’2C’, ’3A’, ’3B’, ’3C’, ’4A’,
  ’4B’, ’4C’]

Let’s also show an example of a triple-nested loop which creates all combinations of given numbers, letters and symbols:

  nums = [’1’, ’2’, ’3’, ’4’]
  chars = [’A’, ’B’, ’C’]
  symbols = [’+’, ’-’]
  combinations = []
  for n in nums:
      for c in chars:
          for s in symbols:
              combinations.append(n + c + s)
  print(combinations)

  [’1A+’, ’1A-’, ’1B+’, ’1B-’, ’1C+’, ’1C-’, ’2A+’, ’2A-’, ’2B+’,
  ’2B-’, ’2C+’, ’2C-’, ’3A+’, ’3A-’, ’3B+’, ’3B-’, ’3C+’, ’3C-’,
  ’4A+’, ’4A-’, ’4B+’, ’4B-’, ’4C+’, ’4C-’]

9.10 Terminating loops with the break statement

The break statement can be used to instantly terminate a for or while loop. It is useful in situations when it no longer makes sense to finish the loop. For example, let’s say that we have a (very long) list of numbers, and it is our task to find out if all of them are positive. In this case, the best solution is to parse the list using a for loop, and terminate the loop when a negative number is found:

  L = [1, 3, -2, 4, 9, 8, 4, 5, 6, 7]
  allpositive = True
  for n in L:
      if n < 0:
          allpositive = False
          break
  print(allpositive)

  False

If the break statement is used within nested loops, then it only terminates the nearest outer one. The following example illustrates that. It is analogous to the previous one, but it analyses a list of lists. You can see that the break statement only terminates the inner loop because the outer loop goes over the four sub-lists and displays the Boolean value of allpositive four times:

  L = [[5, -1, 3], [2, 4], [9, 8, 3], [1, -4, 5, 6]]
  for s in L:
      allpositive = True
      for n in s:
          if n < 0:
              allpositive = False
              break
      print(allpositive)

  False
  True
  True
  False

Importantly, the break statement is just a shortcut. It should be only used to prevent the computer from doing unnecessary operations. Do not get too inventive with this statement. Write your code in such a way that it would work without the break statement too. And finally – make sure to read Subsection 9.12 which is related to the break statement as well.

9.11 Terminating current loop cycle with continue

The continue statement is similar in nature to break, but instead of terminating the loop completely, it only terminates the current loop cycle, and resumes with the next one. As with the break statement, keep in mind that continue is a shortcut. Do not force it where you don’t need it. Unfortunately, you will find many online tutorials which do exactly the opposite. Such as the following program which leaves out a given letter from a given text (we are printing in on red background because it’s not a good code):

  txt = ’Python’
  skipchar = ’h’
  for c in txt:
      if c == skipchar:
          continue
      print(c, end= ’)

  P y t o n

The same result can be achieved way more elegantly without the continue statement:

  txt = ’Python’
  skipchar = ’h’
  for c in txt:
      if c != skipchar:
          print(c, end= ’)

  P y t o n

As a matter of fact, every continue statement can be replaced with a condition. So, the main benefit of using continue is that it "flattens" the code and makes it more readable when multiple nested conditions are present.

Let’s illustrate this on a primitive translator from British to American English, whose sole purpose is to leave out the letter ’u’ if it is following ’o’, and when the next character is not ’n’ or ’s’. First, let’s show the version with the continue statement:

  txt = ’colourful and luxurious country home’
  n = len(txt)
  for i in range(n):
      if txt[i] != ’u’:
          print(txt[i], end=’’)
          continue
      if i > 0 and txt[i-1] != ’o’:
          print(txt[i], end=’’)
          continue
      if i < n-1 and txt[i+1] == ’s’:
          print(txt[i], end=’’)
          continue
      if i < n-1 and txt[i+1] == ’n’:
          print(txt[i], end=’’)

  colorful and luxurious country home

Below is the version without continue. Notice the complicated structure of the nested conditions:

  txt = ’colourful and luxurious country home’
  n = len(txt)
  for i in range(n):
      if txt[i] != ’u’:
          print(txt[i], end=’’)
      else:
          if i > 0 and txt[i-1] != ’o’:
              print(txt[i], end=’’)
          else:
              if i < n-1 and txt[i+1] == ’s’:
                  print(txt[i], end=’’)
              else:
                  if i < n-1 and txt[i+1] == ’n’:
                      print(txt[i], end=’’)

  colorful and luxurious country home

For completeness let us add that the continue statement may only occur syntactically nested in a for or while loop, but not nested in a function or class definition or try statement within that loop.

9.12 The for-else statement

In Python, both the for loop and the while loop may contain an optional else branch. The for-else statement is sort of a mystery, and unfortunately the source of numerous incorrect explanations in online tutorials. There, one can sometimes read that the else branch is executed when the for loop receives an empty sequence to parse. Technically, this is true:

  L = []
  for n in L:
      print(n, end= ’)
  else:
      print(’The else branch was executed.’)

  The else branch was executed.

But the else branch is also executed when the sequence is not empty:

  L = [1, 3, 5, 7]
  for n in L:
      print(n, end= ’)
  else:
      print(’The else branch was executed.’)

  1 3 5 7 The else branch was executed.

So, why would one use the else branch at all?

Well, its purpose is different. As somebody suggested, it should have been named nobreak rather than else, and its purpose would be clear to everybody. Namely, the else branch is executed when the loop has finished regularly (not terminated with the break statement). If the loop is terminated with the break statement, the else branch is skipped. Effectively, this allows us to add code after the for loop which is skipped when the break statement is used. OK, but what is this good for? Let’s show an example.

Imagine that you have a list of values, but some of them can be inadmissible (in our code below, inadmissible = negative). In particular, if all of them are inadmissible, the program cannot go on and needs to terminate with a custom error message. The else branch is perfect for catching this, and throwing that custom error message:

  L = [-1, 2, -3, -4]
  for n in L:
      # If admissible value was found, end the loop:
      if n > 0:
          break
  else:
      raise Exception(’Sorry, all values were negative.’)
  # Now some code that uses the admissible value:
  import numpy as np
  result = np.sqrt(n)
  print(’The result is’, round(result, 4))

  The result is 1.4142

Do not worry about the Exception – for now, it’s there just to throw a custom error message. Exceptions will be discussed in detail in Section 12.

In the "old-fashioned" way, without the else branch, one would have to introduce an extra Boolean variable named (for example) found to find out whether an admissible value was found. And, one would have to use an extra condition to inspect the value of found:

  L = [-1, 2, -3, -4]
  found = False
  for n in L:
      # If admissible value was found, end the loop:
      if n > 0:
          found = True
          break
  if not found:
      raise Exception(’Sorry, all values were negative.’)
  # Now some code that uses the admissible value:
  import numpy as np
  result = np.sqrt(n)
  print(’The result is’, round(result, 4))

  Result is 1.4142

In summary, the use of the else branch has saved us one variable and one condition.

9.13 The for loop behind the scenes – iterables and iterators

At the end of this section we would like to explain the technical details of the for loop, as well as the nature of objects this loop works with. For this, we will need to use some object-oriented terminology, so consider reading Section 14 first.

Let’s begin with a simple for loop:

  s = ’Hi!’
  for c in s:
      print(c, end= ’)

  H i !

The text string s is an iterable object or just iterable, meaning that it has a method __iter__. Other examples of iterable objects in Python are lists, tuples, and dictionaries.

Calling s.__iter__() returns an iterator. Equivalently, the iterator can be obtained by calling iter(s) (the built-in function iter calls the __iter__ method of its argument implicitly).

Behind the scenes, the for statement in the above example calls iter(s). For simplicity, let’s call the returned iterator object iterobj. This object has the method __next__ which can access the elements in the container s one at a time. When there are no more elements, __next__ raises a StopIteration exception which tells the for loop to terminate. The following example shows how the above for loop really works, by creating an iterator object iterobj from the text string s, and repeating the two lines

  c = iterobj.__next__()
  print(c, end= ’)

until the StopIteration exception is thrown:

  s = ’Hi!’
  iterobj = iter(s)
  c = iterobj.__next__()
  print(c, end= ’)
  c = iterobj.__next__()
  print(c, end= ’)
  c = iterobj.__next__()
  print(c, end= ’)
  c = iterobj.__next__()
  print(c, end= ’)

  H i !

  on line 9:
  StopIteration

The last line 10 is not executed because the iteration is stopped on line 9. You can also iterate using the built-in function next which calls __next__ implicitly:

  s = ’Hi!’
  iterobj = iter(s)
  c = next(iterobj)
  print(c, end= ’)
  c = next(iterobj)
  print(c, end= ’)
  c = next(iterobj)
  print(c, end= ’)
  c = next(iterobj)
  print(c, end= ’)

  H i !

  on line 9:
  StopIteration

For completeness, let’s perform the above exercise once more for a sample list L = [1, 2, 3]. First, let’s create the corresponding iterator object and use its method __next__ to iterate through L:

  L = [1, 2, 3]
  iterobj = iter(L)
  n = iterobj.__next__()
  print(n, end= ’)
  n = iterobj.__next__()
  print(n, end= ’)
  n = iterobj.__next__()
  print(n, end= ’)
  n = iterobj.__next__()
  print(n, end= ’)

  1, 2, 3

  on line 9:
  StopIteration

And again, one can use the built-in function next to iterate without revealing the object-oriented nature of the iteration process:

  L = [1, 2, 3]
  iterobj = iter(L)
  n = next(iterobj)
  print(n, end= ’)
  n = next(iterobj)
  print(n, end= ’)
  n = next(iterobj)
  print(n, end= ’)
  n = next(iterobj)
  print(n, end= ’)

  1, 2, 3

  on line 9:
  StopIteration

9.14 Making your own classes iterable

It is easy to add iterator behavior to your classes. Just add an __iter__ method which returns an object with a __next__ method. If your class already has a __next__ method, then __iter__ can just return self:

  class Reverse:
      ~~~
      Iterable for looping over a sequence backwards.
      ~~~
      def __init__(self, data):
          self.data = data
          self.index = len(data)
  
      def __iter__(self):
          return self
  
      def __next__(self):
          if self.index == 0:
              raise StopIteration
          self.index = self.index - 1
          return self.data[self.index]

And now let’s use it:

  rev = Reverse(’Hi!’)
  for c in rev:
      print(c, end= ’)

  ! i H

9.15 Generators

Generators are a simple and powerful tool for creating iterators. They are written like regular functions but use the yield statement whenever they want to return data. Each time the built-in function next is called on it, the generator resumes where it left off - it remembers all the data values and which statement was last executed. Anything that can be done with iterable objects as described in the previous section can also be done with generators. What makes generators so elegant is that the __iter__ and __next__ methods are created automatically. Let’s show an example to make all this more clear.

  def reverse(data):
      for index in range(len(data)-1, -1, -1):
          yield data[index]
  
  rev = reverse(’Hi!’)
  for c in rev:
      print(c, end= ’)

  ! i H

9.16 Functional programming

The more you learn about iterators, the closer you get to functional programming. This is a fascinating programming style and Python provides modules itertools and functools to support it. To learn more, we recommend that you start with the Functional Programming HOWTO in the official Python documentation which can be found at

https://docs.python.org/dev/howto/functional.html#iterators


Table of Contents

Created on August 6, 2018 in Python I,   Python II.
Add Comment
0 Comment(s)

Your Comment

By posting your comment, you agree to the privacy policy and terms of service.