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):
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.
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.
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 ****
‘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
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.
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
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'}
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
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
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
andexcept <error_type>
statements can be used together to deal with known or suspected bugs. This is called error handling in programming. Note that theexcept
statement needs to be given a specific error type in order to work. In the above example, theexcept
statement was made to catch errors of theTypeError
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.
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.
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()
cannotarange()
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’
Pythonarange()
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.
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.
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!
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.
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']
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!