⇦ Back

1 For-Loops

A for-loop will repeat the same operations a given number of times. You can tell the program to perform the same action for a certain number of repetitions. The syntax is as follows:

for number in range(3, 6):
    print(number)
## 3
## 4
## 5

Notice how this code (almost) reads like an English sentence: for each number in the range from 3 to (but not including) 6, print that number. Each number in the range gets assigned to the variable number in turn and used as an input to the print() function.

The range() function can have up to three inputs (all of which must be whole numbers):

  • If you give it one input, it will create a range from 0 to (but not including) that number
  • If you give it two inputs, it will create a range from the first to (but not including) the second
  • If you give it three inputs, it will interpret the third as the step size, ie how many numbers to step over when going from one number to the next
for number in range(3):
    print(number)
## 0
## 1
## 2
for number in range(4, 14, 3):
    print(number)
## 4
## 7
## 10
## 13
for number in range(4, -10, -3):
    print(number)
## 4
## 1
## -2
## -5
## -8

Notice that, in the third example above, the step size was negative and the end of the range was a smaller number that the start, hence it counted down rather than up.

Here’s an example of a for-loop in action: calculating the factorial of a number:

x = 5

# Initialise the answer
answer = 1
# Iterate over the natural numbers below and including x
for num in range(1, x + 1):
    # Multiply the running total by the next number
    answer = answer * num

print(f'{x}! = {answer}')
## 5! = 120

As expected, the factorial of 5 is 120.

1.1 Lists

For-loops work using lists as well. With this object type they iterate over the values: for each value in the list something can be done with that value:

print('MENU:')
# Loop through the values in a list
for meal in ['Pizza', 'Pasta', 'Salad']:
    # Execute the print() function once for each element in the list
    print(meal)
## MENU:
## Pizza
## Pasta
## Salad

The above code printed each element of the list in turn, as opposed to the following code which prints all the elements in one go:

meals = ['Pizza', 'Pasta', 'Salad']
# Execute the print() function once on the whole list
print(meals)
## ['Pizza', 'Pasta', 'Salad']

If you instead want to iterate over the indexes of a list, you can create a range from the length of the list:

print('MENU:')
# Create a list
ls = ['Pizza', 'Pasta', 'Salad']
# Loop the same number of times as there are elements in the list
for idx in range(len(ls)):
    print(idx, ls[idx])
## MENU:
## 0 Pizza
## 1 Pasta
## 2 Salad

To do both and iterate over the indexes and the values, use the enumerate() function. This returns a tuple for each element in the list containing that element’s index and its value in that order:

print('MENU:')
# Create a list
ls = ['Pizza', 'Pasta', 'Salad']
# Iterate over both the indexes and values in a list
for idx, value in enumerate(ls):
    print(idx, value)
## MENU:
## 0 Pizza
## 1 Pasta
## 2 Salad

A for-loop can also be run on one line:

# Create a list
ls = [
    6.28318,
    0.57721,
    9.81,
]
# The formatter ".1f" will round to one decimal place
print('\n'.join(f'{v:.1f}' for v in ls))
## 6.3
## 0.6
## 9.8

The above example shows how to format an entire list of numbers by using a combination of an f-string, a for-loop and the .join() method.

1.1.1 Conditionals (If-Statements)

A for-loop can be combined with an if-statement to only execute on certain values from the list:

numbers = [5, 3, 8, 1, 3, 5, 9, 4]
# Only print the large numbers
for number in numbers:
    if number > 7:
        print(number)
## 8
## 9

A more efficient way to do this, however, is to use a list comprehension (read more about list comprehensions on this page):

numbers = [5, 3, 8, 1, 3, 5, 9, 4]
# Only print the large numbers
for number in [v for v in numbers if v > 7]:
    print(number)
## 8
## 9

…or even:

numbers = [5, 3, 8, 1, 3, 5, 9, 4]
# Only print the large numbers
[print(v) for v in numbers if v > 7]
## 8
## 9

Here’s another example of a for-loop with a conditional, in this case being used to censor certain words:

text = "Frankly, my dear, I don't give a damn"
to_censor = 'damn'

words = text.split()
for i in range(len(words)):
    if words[i] == to_censor:
        words[i] = '*' * len(to_censor)

print(' '.join(words))
## Frankly, my dear, I don't give a ****

1.1.2 Two Lists

‘Zipping’ two lists together will allow you to look at both of them at the same time. However, if one list is longer than the other it will be shortened so that they match:

list1 = [3, 9, 17, 15, 19]
list2 = [2, 4, 8, 10, 30, 40, 50, 60, 70, 80, 90]

# Determine which number at each position in the lists is larger
for a, b in zip(list1, list2):
    if a > b:
        print(a)
    else:
        print(b)
## 3
## 9
## 17
## 15
## 30

In the above example, each of the five numbers in the first list was compared to the corresponding number in the second list; the remaining six numbers in the second list were discarded. If we instead want to loop through each combination of elements from both lists we need to use a nested list comprehension:

list1 = ['A', 'B']
list2 = ['1', '2', '3', '4']

combinations = [(x, y) for x in list1 for y in list2]
for combination in combinations:
    print(combination[0], combination[1])
## A 1
## A 2
## A 3
## A 4
## B 1
## B 2
## B 3
## B 4

1.1.3 Getting the Next Element

The next() function can be used as a more powerful alternative to indexing/finding/searching a list. This function looks at each element in a list in turn, starting at the beginning, and for each one determines if it matches a given criterion. The first element it finds that does so will be returned in the manner of your asking. For example, you can ask the function to return the “next value, out of all the values in the list, where the value is equal to ‘B’”. This looks as follows:

ls = ['A', 'B', 'C', 'D', 'E']
print(next(value for value in ls if value == 'B'))
## B

Alternatively, if your list is made up of numbers, you can ask the function to return the “next value, out of all the values in the list, if the value is greater than 5”:

ls = [2, 4, 6, 8, 10]
print(next(value for value in ls if value > 5))
## 6

In this case, the first value that matched was “6”, which is what was returned. You also have the option to ask the function to return the element to you in an augmented manner, eg as a string:

ls = [2, 4, 6, 8, 10]
print(next(f'The next number >5 is: {value}' for value in ls if value > 5))
## The next number >5 is: 6

It is also possible to get the index of the element that first matches your criterion as opposed to the value. This requires the enumerate() function, which splits each element in a list up into its index and its value. As such, you need to ask for the “next index, given each index and value in the list, if the value is greater than 5”:

ls = [2, 4, 6, 8, 10]
print(next(index for index, value in enumerate(ls) if value > 5))
## 2

The element at index position 2 (which has the value “6”) was the first to match the criterion of being greater than 5.

As the name suggests, the next() function only finds the next element that meets the given criterion and, as such, can only ever return one element (or zero elements if there are none that meet the criterion). If you are instead interested in finding all the elements in the list you will need to use a list comprehension.

1.2 Strings

Strings are treated as lists of letters:

# Loop through the letters in a word
for letter in 'HELLO':
    print(letter)
## H
## E
## L
## L
## O

Setting the end keyword argument in the print() function to an empty string causes the letters to print on the same line. This is because it overwrite the default value for end which is the newline character (\n):

# Loop through the letters in a word
for letter in 'HELLO':
    print(letter, end='')
## HELLO

1.3 Dictionaries

By default, a for-loop will iterate over a dictionary’s keys:

dct = {'a': 'apple', 'b': 'banana', 'c': 'cat'}
# Loop through the keys in a dictionary
for key in dct:
    # Print the key and the corresponding value
    print(key, dct[key])
## a apple
## b banana
## c cat

Alternatively, the loop can iterate over the key-value pairs - use the .items() method to access the items inside of the dictionary for this purpose:

for key, value in dct.items():
    print(key, value)
## a apple
## b banana
## c cat

If an empty dictionary has been initialised, it can be populated within a for-loop:

dct = {}
for day in ['Mon', 'Tues', 'Wednes', 'Thurs', 'Fri', 'Satur', 'Sun']:
    dct[day[:3]] = day + 'day'

print(dct)
## {'Mon': 'Monday', 'Tue': 'Tuesday', 'Wed': 'Wednesday', 'Thu': 'Thursday', 'Fri': 'Friday', 'Sat': 'Saturday', 'Sun': 'Sunday'}

2 Statements

2.1 Break

The break statement will cause Python to break out of the loop and continue on with the rest of the script:

# Iterate over the range from 0 to 10
for i in range(11):
    # Once you get to 5, break out of the loop
    if i >= 5:
        break
    print(i)
print('The rest of the script will now run')
## 0
## 1
## 2
## 3
## 4
## The rest of the script will now run

2.2 Continue

The continue statement will cause Python to skip the rest of the current iteration of the loop and continue with the next iteration:

# Iterate over the range from 0 to 10
for i in range(11):
    # When you have an even number, skip to the next iteration
    if i % 2 == 0:
        continue
    print(i)
print('The rest of the script will now run')
## 1
## 3
## 5
## 7
## 9
## The rest of the script will now run

2.3 Pass

The pass statement will cause Python to do nothing! It can be used with for-loops to pass over errors without bringing the entire script to a halt:

# This list contains 5 numbers and a string
ls = [0, 1, 2, 3, 'four', 5]
# Iterate over the elements in the list
for x in ls:
    try:
        # Try to perform division
        print(x / 2)
    except TypeError:
        # If you get an error, do nothing
        pass
print('The rest of the script will now run')
## 0.0
## 0.5
## 1.0
## 1.5
## 2.5
## The rest of the script will now run

When the code tried to divide ‘four’ by 2 it created an error (because a string cannot be divided by 2!). However, because the try and except statements were used, instead of this error stopping the entire script it caused the pass statement to execute. In this way, we allowed the script to continue to run despite the error being there.

The try and except <error_type> statements can be used together to deal with known or suspected bugs. This is called error handling in programming. Note that the except statement needs to be given a specific error type in order to work. In the above example, the except statement was made to catch errors of the TypeError variety (ie when a variable is of the wrong type) which is what gets raised when you try to divide a string instead of a number by 2. An error of a different type would not have been caught.

Alternatively, the pass statement can be used to avoid using negative conditions (eg not in) which can be confusing. Here’s an example that loops over a string and removes any vowels it encounters by using an if-statement to find them:

word = 'Hello, World'
new_word = ''
# Iterate through the letters in the word
for character in word:
    if character in 'aeiouAEIOU':
        # If a vowel is encountered, do nothing
        pass
    else:
        # If not a vowel, add the letter to the word that will be outputted
        new_word = new_word + character
print(new_word)
## Hll, Wrld

It would have been possible to write this function with just one conditional (ie if character not in) but the programme would have lost readability. From a stylistic point of view, it’s best to avoid negative conditionals.

2.4 Else (For-Else Loops)

A ‘for-else’ loop is a combination of a for-loop, an if-statement and an else-statement. The for-loop will run, then the else-statement will run unless you break out of the for-loop:

import random

# Set the seed so that we get the same random numbers each time
random.seed(20210830)

print('Lucky Numbers! 3 numbers will be generated.')
print('If one of them is a "5", you lose!')
for i in range(3):
    num = random.randint(1, 6)
    print(num)
    if num == 5:
        print('Sorry, you lose!')
        break
else:
    print('You win!')
## Lucky Numbers! 3 numbers will be generated.
## If one of them is a "5", you lose!
## 1
## 4
## 1
## You win!

This functions a bit like a flag: if you get a “5”, a ‘flag’ is raised that causes some code to execute but other code to not execute.

3 Range vs Arange

The Numpy package contains a function called arange() which, at first glance, appears to be identical to the range() function that has been used above:

for x in range(3, 12, 4):
    print(x)
## 3
## 7
## 11
import numpy as np

for x in np.arange(3, 12, 4):
    print(x)
## 3
## 7
## 11

The behaviour of arange() is equivalent to that of range() except:

  • arange() creates an array\(^1\) whereas range() creates something called a range object. The difference is that an array contains all the numbers in the given range - ie all the numbers are stored explicitly in memory - while a range object only contains the information about the range which it then uses to generate\(^2\) each successive number in the sequence on demand.
  • arange() can have non-integers as arguments whereas range() cannot
  • arange() is part of the Numpy library (which needs to be downloaded at some point after you first install Python and which needs to be imported into every script you write which uses arange()) whereas range() is part of the standard library and so it is immediately available in ‘base’ Python
  • arange() is more efficient for matrix manipulation while range() is more efficient for for-loops

\(^1\)Specifically, arange() creates an ndarray object - an n-dimensional array
\(^2\)There are things in Python known as “generators” and range() is NOT a generator, but it is similar in that it generates numbers on demand as opposed to creating and storing them all at once like arange() does

Both list(arange()) and list(range()) will produce a list of the numbers in the given sequence. Depending on the context, a list, array or range object might be more appropriate.

3.1 Speed Test

It was mentioned above that arange() is more efficient for matrix manipulation while range() is more efficient for for-loops. We can confirm this by using the “timeit” module to time how quickly code takes to run: firstly, here’s a simple for-loop (for x in range(100): pass) run 100,000 using that standard library’s range():

from timeit import timeit

n = int(1e5)
t = timeit(stmt='for x in range(100): pass', number=n) / n * 1e6
print(f'The code ran {n:,} times and took {t:4.2f} microseconds on average')
## The code ran 100,000 times and took 0.66 microseconds on average

Now, here is the same loop run 100,000 using Numpy’s arange():

t = timeit(
    stmt='for x in np.arange(100): pass',
    setup='import numpy as np',
    number=n
) / n * 1e6
print(f'The code ran {n:,} times and took {t:4.2f} microseconds on average')
## The code ran 100,000 times and took 4.40 microseconds on average

This confirms that range() is quicker for for-loops. Now let’s try matrix manipulation: how long does it take to double all the numbers from 0 to 99 using range()?

n = int(1e5)
t = timeit(stmt='[x * 2 for x in range(100)]', number=n) / n * 1e6
print(f'The code ran {n:,} times and took {t:4.2f} microseconds on average')
## The code ran 100,000 times and took 2.08 microseconds on average

…and using arange()?

t = timeit(
    stmt='np.arange(100) * 2',
    setup='import numpy as np',
    number=n
) / n * 1e6
print(f'The code ran {n:,} times and took {t:4.2f} microseconds on average')
## The code ran 100,000 times and took 1.42 microseconds on average

This time, Numpy’s arange() was faster.

4 While-Loops

A ‘while-loop’ will continue performing a set of operations until a condition is met. Let’s imagine you have a jug that is 1 litre in size and you are pouring 150 ml glasses of water into it. You want to continue doing this as many times as you can without letting the jug overflow. Let’s set up the scenario:

water_in_jug = 0
capacity = 1000
glass_size = 150
number_of_pours = 0

You start with 0 ml of water in the jug. The capacity of the jug is 1000 ml. The size of the glass you are pouring in is 150 ml. You start having poured this glass into the jug 0 times. Now start pouring:

# This loop will continue to run while the amount of water in the jug is under
# the capacity
while water_in_jug < capacity:
    # Pour a glass
    water_in_jug = water_in_jug + 150
    number_of_pours = number_of_pours + 1

print(number_of_pours)
print(water_in_jug)
print(water_in_jug > capacity)
## 7
## 1050
## True

Whoopsie! You poured 7 glasses worth of water into the jug and caused it to overflow by 50 ml! Think about how you would modify the code in order to stop it from pouring too much. There are a couple of ways to do it, but one option is to tell the loop to run forever and then stop it once it nears capacity:

water_in_jug = 0
capacity = 1000
glass_size = 150
number_of_pours = 0

# This loop will continue to run for as long as True is True (ie forever)
while True:
    # If the jug is almost at capacity, stop
    if water_in_jug > capacity - glass_size:
        break
    # Pour a glass
    water_in_jug = water_in_jug + glass_size
    number_of_pours = number_of_pours + 1

print(number_of_pours)
print(water_in_jug)
print(water_in_jug > capacity)
## 6
## 900
## False

Hurray! No overflow!

4.1 Using a While-Loop as a For-Loop

Let’s continue using the above example. Now that we know that 6 pours is the most we can perform before the jug overflows, we could use a for-loop and set it to run exactly six times:

for i in range(6):
    # Pour a glass
    number_of_pours = i + 1
    water_in_jug = number_of_pours * glass_size

print(number_of_pours)
print(water_in_jug)
print(water_in_jug > capacity)
## 6
## 900
## False

However, we could also use a while-loop and set it to run exactly six times:

water_in_jug = 0
capacity = 1000
glass_size = 150
number_of_pours = 0

while number_of_pours < 6:
    # Pour a glass
    water_in_jug = water_in_jug + glass_size
    number_of_pours += 1

print(number_of_pours)
print(water_in_jug)
print(water_in_jug > capacity)
## 6
## 900
## False

Note that in the above example we used number_of_pours += 1. This is shorthand for number_of_pours = number_of_pours + 1.

The opposite (using a for-loop as a while-loop) is possible in the sense that we can make a for-loop run a large number of times and have it break out when a condition is met:

water_in_jug = 0
capacity = 1000
glass_size = 150
number_of_pours = 0

for pours in range(100):
    # Pour a glass
    water_in_jug = pours * glass_size
    number_of_pours = pours
    # Check if you are nearing capacity
    if water_in_jug + glass_size > capacity:
        break

print(number_of_pours)
print(water_in_jug)
print(water_in_jug > capacity)
## 6
## 900
## False

Note that the fact that the break statement has been put at the end of the loop means that the ‘pour a glass’ code will run even on the last iteration. If you instead put the break statement at the start, on the last iteration it will not run.

4.2 While What?

On what will a while-loop run (what a confusing sentence!)? By this I mean “what can be used to make a while-loop work”? Well, we know it will run on a conditional statement that evaluates to True, such as a statement using ‘greater than’ (>), ‘less than’ (<) or ‘equal to’ (==), and stop when it becomes False:

i = 0
while i < 3:
    i += 1
    print(f'This has looped {i} time(s)')
## This has looped 1 time(s)
## This has looped 2 time(s)
## This has looped 3 time(s)

It will also run on True itself and only stop when you break out of it:

i = 0
while True:
    i += 1
    print(f'This has looped {i} time(s)')
    if i >= 3:
        break
## This has looped 1 time(s)
## This has looped 2 time(s)
## This has looped 3 time(s)

It will also run on a non-empty list and stop when it becomes empty:

todo_list = ['wash', 'dry', 'iron']
while todo_list:
    print('To do:', todo_list)
    _ = todo_list.pop()
## To do: ['wash', 'dry', 'iron']
## To do: ['wash', 'dry']
## To do: ['wash']

4.3 While-Else Loops

A ‘while-else’ loop is a combination of a while-loop, an if-statement and an else-statement. The while-loop will run, then the else-statement will run unless you break out of the while-loop:

import random

# Set the seed so that we get the same random numbers each time
random.seed(20210830)

print('Lucky Numbers! 3 numbers will be generated.')
print('If one of them is a "5", you lose!')
count = 0
while count < 3:
    num = random.randint(1, 6)
    print(num)
    if num == 5:
        print('Sorry, you lose!')
        break
    count += 1
else:
    print('You win!')
## Lucky Numbers! 3 numbers will be generated.
## If one of them is a "5", you lose!
## 1
## 4
## 1
## You win!

5 Summary

  • Loops can be used to do the same thing multiple times
  • There are for-loops which repeat the same thing a set number of times
  • There are also while-loops which repeat the same thing until a condition is met

⇦ Back