Back to Contents

Hints may be found after all the answers.

## 1. Starting Off

1

The expression 17 is a number and so is a value already – there is no work to do. The expression 1 + 2 * 3 + 4 will evaluate to the value 11, since multiplication has higher precedence than addition. The expression 400 > 200 evaluates to the boolean True since this is result of the comparison operator > on the operands 400 and 200. Similarly, 1 != 1 evaluates to False. The expression True or False evaluates to True since one of the operands is true. Similarly, True and False evaluates to False since one of the operands is false. The expression ’%’ is a string and is already a value.

2

The expression evaluates to 11. The programmer seems to be under the impression that spacing affects precedence. It does not, and so this use of space is misleading.

3

The % operator is of higher precedence than the + operator. So 1 + 2 % 3 and 1 + (2 % 3) are the same expression, evaluating to 1 + 2 which is 3, but (1 + 2) % 3 is the same as 3 % 3, which is 0.

4

The comparison operator < considers the words in dictionary order, so ’bacon’ < ’eggs’. The uppercase letters are all “smaller” than the lowercase characters, so for example ’Bacon’ < ’bacon’ evaluates to True. For booleans, False is considered “less than” True.

5

The first one is, of course entirely as expected:

Python
>>> 1 + 2
3

It turns out that the + operator we have been using on numbers to add them can be used on strings to concatenate them:

Python
>>> 'one' + 'two'
'onetwo'

However, it will not work to mix the two types:

Python
>>> 1 + 'two'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

The * operator can also be used on a string and a number, to concatenate the string multiple times:

Python
>>> 3 * '1'
'111'
>>> '1' * 3
'111'
>>> print('1' * 3)
111

In the last line, we remember the difference between a string and printing a string. When it is (ab)used as a number, True has the value 1, whereas False has the value 0:

Python
>>> True + 1
2
>>> False + 1
1

Did you notice the f before the quotation mark in the last example? This is a format string, which we will discuss in chapter 6:

Python
>>> print(f'One and two is {1 + 2} and that is all.')
One and two is 3 and that is all.

The part between the curly braces {} has been evaluated and then printed.

## 2. Names and Functions

1

We include the return keyword to make sure the result is returned to us:

Python
>>> def times_ten(x):
...     return x * 10
...
>>> times_ten(50)
500

2

This function will have two arguments. We use the and operator, together with the inequality operator !=.

Python
>>> def both_non_zero(a, b):
...     return a != 0 and b != 0
...
>>> both_non_zero(1, 2)
True
>>> both_non_zero(1, 0)
False

3

This is a simple function with three arguments. We remember to use return, of course:

Python
>>> def volume(w, h, d):
...    return w * h * d
...
>>> volume(10, 20, 30)
6000

We can now write our volume_ten_deep function:

Python
>>> def volume_ten_deep(w, h):
...     return volume(w, h, 10)
>>> volume_ten_deep(5, 6)
300

Notice that we need return here too: the return in volume will not suffice.

4

If a lower case character in the range ’a’’z’ is not a vowel, it must be a consonant. So we can reuse the is_vowel function we wrote earlier, and negate its result using not:

Python
>>> def is_consonant(s):
...     return not is_vowel(s)
...
>>> is_consonant('r')
True
>>> is_consonant('e')
False

5

We could simply return 0 for a negative argument. The factorial of 0 is 1, so we can change that too, and say our new function finds the factorial of any non-negative number:

Python
>>> def factorial(x):
...     if x < 0:
...         return 0
...     elif x == 0:
...         return 1
...     else:
...         return x * factorial(x - 1)
...
>>> factorial(-1)
0

6

We can use a recursive function:

Python
>>> def sum_nums(n):
...     if n == 1:
...         return 1
...     else:
...         return n + sum_nums(n - 1)
...
>>> sum_nums(10)
55

There is a direct mathematical formula too. We use the integer division operator //, which we have not yet seen:

Python
>>> def sum_nums(n):
...     return (n * (n + 1)) // 2
>>> sum_nums(10)
55

Can you see why?

7

A number to the power of 0 is 1. A number to the power of 1 is itself. Otherwise, the answer is the current n multiplied by nx − 1.

Python
>>> def power(x, n):
...     if n == 0:
...         return 1
...     else:
...         if n == 1:
...             return x
...         else:
...             return x * power(x, n - 1)
...
>>> power(2, 5)
32

Notice that we had to put one if and else inside another here. The indentation helps to show the structure. Remembering that Python allows us to compress this using the elif keyword:

Python
>>> def power(x, n):
...     if n == 0:
...         return 1
...     elif n == 1:
...         return x
...     else:
...         return x * power(x, n - 1)
...
>>> power(2, 5)
32

This is easier to read, partly because all the return keywords line up. In fact, we can remove the case for n = 1 since power(x, 1) will reduce to x * power(x, 0) which is just x.

8

We test each number less than the given number for divisibility using the % modulus operator we learned about in chapter 1, increasing the test divisor by one each time. If it is divisible, we print it.

Python
>>> def factors(n, x):
...  if x % n == 0: print(n)
...  if n < x: factors(n + 1, x)
>>> factors(1, 12)
1
2
3
4
6
12

We can clean the solution up by wrapping it in another function which supplies the starting point of 1:

Python
>>> def factors_simple(x):
...   factors(1, x)
>>> factors_simple(12)
1
2
3
4
6
12

## 3. Again and Again

1

We set the step to  − 1. We must be careful with the start and stop points. We set the stop point to 0 so that we stop at 1 (i.e. before we get to 0):

2

We change the calculation of column_width to take n * (n - 1) as the item with maximum width rather than n * n:

Here is the new result for a table of size ten:

Python
>>> times_table(10)
1  2  3  4  5  6  7  8  9  10
2  4  6  8  10 12 14 16 18 20
3  6  9  12 15 18 21 24 27 30
4  8  12 16 20 24 28 32 36 40
5  10 15 20 25 30 35 40 45 50
6  12 18 24 30 36 42 48 54 60
7  14 21 28 35 42 49 56 63 70
8  16 24 32 40 48 56 64 72 80
9  18 27 36 45 54 63 72 81 90
10 20 30 40 50 60 70 80 90 100

It is still possible to have excess space in some columns with this method:

Python
>>> times_table(4)
1  2  3  4
2  4  6  8
3  6  9  12
4  8  12 16 

Can you fix this?

3

We use a for loop to check each letter in the string, adding one to a local variable each time we see a space:

We use return to make sure the final count is the result of the function. We can use the += operator to shorten the common operation of adding to a variable:

This works for other operators too.

4

This is a classic problem. It sounds easy, and yet requires several changes. We calculate the length of the string with len, so we know how many letters there are left to go. Then, in the for loop itself, we deduct one from that count each time, and print the space only if the count indicates we are not on the last letter.

5

A very simple while loop is required:

6

We supply the prompt directly to the input function. We must add the newline \n because, unlike print, input does not move to the next line after printing the prompt.

We can now remove the entered variable, but we must use pass, otherwise the while statement would be ungrammatical in Python:

7

We need three variables: target to hold the secret number between 1 and 100, guess to hold the current guess, and tries to count the number of tries.

Inside the while loop, we add one to the number of tries, and keep going until the correct answer is guessed. Then we print the final message with the number of tries. Notice that we have use an if and an elif but no else. Can you simplify the conditional construct further?

8

We use one giant if construct (in the next chapter we shall discuss better ways to do this). The function print_morse_letter prints a single code for the given letter, followed by three spaces:

Now the main function, to print a whole string, uses print_morse_letter for each letter which is not a space. For spaces it prints an extra four spaces in the output to add to the three following the previous letter.

This implementation has two problems: (1) it prints an extra three spaces at the end of any message not ending with a space; and (2) a space at the beginning of a message will have only four spaces in the output not seven. Can you fix them?

## 4. Making Lists

1

The first function is simple: we just return the element at index 0. To return the last element we must use the len function to calculate the index. We remember to subtract one, since list indices start at zero.

In fact, we can also write l[-1] to retrieve the last element of a list in Python. What happens in each case if the list is empty?

2

We first create a fresh, empty list. Then we can iterate over the input list in order, inserting each element at index 0 in the new list. The effect is to produce a reversed list, which is then returned.

Alternatively, we can use a range with a negative step value, in conjunction with the append method. Again, we begin with a fresh, empty list.

3

We will use a for loop to look at each element, updating two variables to keep track of the smallest and largest numbers seen so far. The important thing to to properly initialise the minimum and maximum variables. We do so by setting them to be equal to the first element of the list. Can you see why?

This function has a minor inefficiency; it looks at the first element of the list twice. Can you fix that?

4

We need a step value of two, and start and stop values encompassing the whole list:

Of course, such start and stop values are the default, so we may also write:

5

We follow the pattern of our second, shorter evens function above, and write simply:

6

We begin by making a fresh, empty list. Then, for each element in the original list, we add it to the new list, unless it is already there:

7

We use our setify function to make a new list of the unique items in the original list. Then we iterate over this new list, finding the number of each of its elements in the original list using the count method, and print it.

8

This is a simple exercise in the use of in and the boolean operator and:

9

We create a fresh, empty list and append the elements of the original list one by one:

Alternatively, we can use the slice operator with empty start and stop values:

10

We make a fresh list with our new copy_list function, remove the value using remove and then return the new list:

11

We set up our alphabet and use list slicing to write a function rotate which can rotate it by any number of places from 1 to 25, returning a new string:

Now our encoding and decoding functions are simple, taking the text to encode or decode, and the rotated cipher. They are, as you would expect, somewhat symmetrical:

We remember to treat spaces specially.

12

Now that we know about Python’s lists, we can dispense with the huge if construct of our previous Morse code solution, and work from two lists: the letters and their codes:

Now it is simple to modify our previous solution to look up codes in the list using the index method on lists:

13

First, we shall write a function which looks at a single guess and calculates how many are a) the correct number in the correct place and b) the correct number in the incorrect place. This is surprisingly delicate, since we must make sure not to double count anything – for example, if the code is 1441 and the guess is 4444 we should identify two 4s as being correct numbers in the correct places, but the other two 4s are not identified as being correct numbers in the wrong place, because we have already used up all the 4s in our code.

Our function will take two lists of four numbers each, the code and the guess. We copy them, since we will be changing values in the lists to mark them as used. Then we set up two counters, to keep track of how many numbers are correct and in the right place, or correct and in the wrong place.

The function returns True if the code is completely correct, and False otherwise. Now we can write the main function which asks the user for a guess repeatedly:

There is currently no handling of errors here - what happens if you type in too few or too many numbers, for instance? Can you fix the program to handle this?

## 5. More with Lists and Strings

1

We split to make a list, then the sort method to sort it in place.

2

Using the sorted function removes the need to introduce the intermediate name l as in the previous question:

This makes our function a little easier to read.

3

Here is the original from chapter 4:

The modification is very simple: we use the sorted function when creating our list of unique values:

4

If we write a function to remove any spaces at the front of a list of strings, we can then use it multiple times to deal with spaces at the beginning and end. Here is such a function:

For example, strip_leading_spaces([’ ’, ’ ’, ’y’, ’e’, ’s’, ’ ’]) will return [’y’, ’e’, ’s’, ’ ’]. Notice that the and operator does not try its right hand side if its left hand side is false. And so, if len(l) > 0 is True, the first element of l will not be tested for equality, and the function succeeds even when the list is empty (or consists only of spaces).

Now we can write the main function, which uses our stripper twice, to remove the spaces at the beginning and end of the list made from the original string. One final reversal brings it back to the correct order, and we join it back into a string.

5

We can (ab)use split and join to make a much simpler definition:

This will, however, also remove any excess multiple spaces in between words. Python provides a built-in method strip to remove just the parts at either end, leaving the rest untouched.

6

First, our clip function:

Now, we use map with our clip function, not forgetting to use list to get back an ordinary list before returning.

7

We have already seen how a slice with a step of -1 may be used to reverse a list. A palindrome is something which equals its own reverse, so it is easy to write out the definition:

We can use this is_palindromic function as a filter to return only such strings in a list as are palindromic:

Now, we can build only those numbers in a range whose strings are palindromic.

We must remember to convert them back to integers before returning. We can achieve this with map, of course. Now we can find all the palindromic numbers up to 500:

>>> palindromic_numbers_in(1, 500)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66,
77, 88, 99, 101, 111, 121, 131,141, 151, 161, 171,
181, 191, 202, 212, 222, 232, 242, 252, 262, 272,
282, 292,303, 313, 323, 333, 343, 353, 363, 373, 383,
393, 404, 414, 424, 434, 444, 454, 464, 474, 484, 494]

We can remove the instances of list since range, filter, and map are happy to accept iterators:

We have to chosen to return an actual list, not a generator, from the final palindromic_numbers_in function, however.

8

This is a simple list comprehension, just using clip on each item in the list:

9

In this example, we use both for and if in a list comprehension to process the list of strings.

It is just about readable to put the whole thing in one expression:

## 6. Prettier Printing

1

We must keep a counter to prevent the printing of an extra comma and space:

This is a lot more complicated than having print do the work for us, but it does allow us some customisation: for example if we wished to print without commas, or without spaces.

2

In this instance, format strings do not really help use remove any complexity. In fact, this solution is slightly longer than the one without format strings.

3

Without format strings, we must use str explicitly on each integer, as a prelude to calling the rjust method. We can use the print function with multiple arguments to print a whole line, though:

4

This is a simple substitution of zfill for rjust:

In fact, we can use rjust(5, ’0’) to achieve the same effect.

5

We use the with … as construct to safely open and close the file. Then it is a matter of carefully constructing the while condition and the variable it sets to get the right result. We then rearrange the parts of the name the user types in, and print it to file:

Can you see why we had to initialise the name variable to a non-empty string?

6

Using a format string allows us to remove the sep=’, ’ argument, but not a lot else:

7

For each sentence in the list, we find the position (if any) of the word. Remembering that failure to find the word results in a position of -1, we decide what to print to the screen:

8

This is a simple alteration to the previous answer:

## 7. Arranging Things

1

This can be achieved by tuple unpacking:

Python
>>> a = 1
>>> b = 2
>>> a, b = (b, a)
>>> a
2
>>> b
1

Note we do not need parentheses on the tuple when doing multiple assignment of values to names:

Python
>>> a = 1
>>> b = 2
>>> a, b = b, a
>>> a
2
>>> b
1

However, we cannot write this:

Why not?

2

We can use the items method on the dictionary, which allows iterating with a for loop using two variables, one for the key and one for the value. We return the result as a tuple.

For example:

Python
>>> unzip({1: 'one', 2: 'two'})
([1, 2], ['one', 'two'])

3

We initialise a fresh, empty dictionary. Then, looping over the index positions in the list of keys, we add each key and its value to the dictionary.

What happens if the lists ks and vs are of differing lengths?

4

Beginning with an empty dictionary, we loop over the items in each existing dictionary, adding the key and its associated value to the union dictionary.

The preference for values from dictionary a is achieved by processing it second. Duplicate entries from dictionary b are thus overwritten.

5

The list is being modified by deletion during the for loop, and so the indices change. Here is a possible working version, which repeatedly uses the remove method which, we remember, removes the first instance of a given element in a list:

6

We need to assign values to two names here, so we use the items method. The rest is then simple:

The output is not always the same length as the input, because a value may appear multiple times in the input, and so be used multiple times as a key in the output:

Python
>>> reverse_dict({1: 2, 2: 1, 3: 1})
{2: 1, 1: 3}

7

We remember that an empty set is created by set(). We loop over the input words, using set again to build a set of all the letters in each word, and the | operator to add them to our master set, which we then return:

For example:

Python
>>> letter_set(['one', 'two', 'three'])
{'w', 'n', 't', 'h', 'o', 'r', 'e'}

To do the inverse, we shall need a set of all the letters. Then we can use the set difference operator.

For example:

Python
>>> letters_not_used(['one', 'two', 'three'])
{'m', 'd', 'f', 'q', 'l', 'y', 's', 'k', 'g', 'c',
'v', 'j', 'p', 'a', 'u', 'z', 'x', 'b', 'i'}

8

We can represent sets using dictionaries with the values ignored, for example all set to zero. Here is a function to build such a ‘set’ from a list:

Now we can implement the operations. First, for the ‘or’ operation, we add entries to the new dictionary from both input lists:

For ‘and’, we must check that the item is in both sets:

Set difference is very similar:

Finally, exclusive or can be achieved by using our existing functions:

9

We use two for portions to iterate over both input sets. Only when x == y do we have a match.

This code checks every possible combination of elements of a and b and so is not very efficient.

10

If the type of the input value t is an integer, we return it. Otherwise, we loop over all the items in t, adding up their sums by recursive application of the sum_all function itself.

The result works on any tuple containing only number and on numbers themselves:

Python
>>> sum_all((1, 2, 3))
6
>>> sum_all((1, (1, 2), 3))
7
>>> sum_all(10)
10

## 8. When Things Go Wrong

1

We handle the ValueError resulting from int being used on a string which cannot reasonably be converted to an integer, and ignore the error by using pass:

Since the exception is raised by int, the total variable will not be updated in the case of a bad string. So we need not worry about the += operation receiving a bad input.

2

We write two little functions. First, safe_int, which handles the ValueError exception raised by int and returns None instead. Second, the function not_none which returns True if a value is anything other than None. Then we can apply map and filter to build a list of results from safe_int and filter out the None values.

3

We handle the ZeroDivisionError exception, returning 0.

4

We begin with a fresh dictionary. Then, we iterate over the keys and values of dictionary a. We try to insert the corresponding value from b into the new dictionary. If it fails, we handle KeyError and simply skip that key.

5

It is easy to add all the items from our first dictionary to the new one – the keys are already unique. When we add items from the second, we check to see if the key exists already. If it does, we raise KeyError.

6

We check to see if the item is already in the set. If it is, we raise KeyError. If not, we add it as usual.

## 9. More with Files

1

The with … as construct allows us to combine the two statements in the original into a single block:

2

We use with … as again, using the optional file argument of the print function to write each key and value:

3

This is a good example of the complications of reading from a file, expecting entries in a certain format, and finding data not fitting such a format. We begin with an empty dictionary, and then enter a while True loop. We then try to read keys and values, returning if we have reached the end of the file (or if the line is empty).

The ValueError exception which may be raised is caught and a message is printed.

4

We open the two input files, and the output file in ‘append’ mode. Then it is as simple as copying the lines across, being sure not to introduce extra newlines.

5

We use read to get the whole contents of the file at once, split it into ‘words’, then convert them to integers with map and sum them:

6

This is similar to our append_files function from question 4:

7

We introduce a dictionary to store the character histogram, then create or increment an entry in the dictionary for each character encountered.

8

For the word histogram, we introduce a function clean_split which splits a line into words, then processes each word to remove punctuation, and convert to lowercase.

9

We can reuse clean_split here to get the words in each line. Then, we can use enumerate to iterate over the indices and lists of words for each line. We check for the presence of the search term, and print the line and its number if required.

10

We read the lines all at once with readlines. Then, by careful use of slices, we print five at a time, waiting for the user to press Enter.

How might this be rewritten to work well on huge files? In that case, reading all the lines at once would be inefficient.

## 10. The Other Numbers

1

We calculate the ceiling and floor, and return the closer one, being careful to make sure that a point equally far from the ceiling and floor is rounded up.

2

The function returns another point, and is simple arithmetic.

3

The whole part is calculated using the floor function. We return a tuple, the first number being the whole part, the second being the original number minus the whole part. In the case of a negative number, we must be careful – floor always rounds downward, not toward zero!

Notice that we are using the unary operator - to make the number positive.

4

We need to determine at which column the asterisk will be printed. It is important to make sure that the range 0…1 is split into fifty equal sized parts, which requires some careful thought. Then, we just print enough spaces to pad the line, add the asterisk.

5

Our function takes another function as one of its argument. We use a variable to hold the current value, starting at the beginning of the range, and then loop until we are outside the range.

No allowance has been made here for bad arguments (for example, b smaller than a). Can you extend our program to move the zero-point to the middle of the screen, so that the sine function can be graphed even when its result is less than zero?

## 11. The Standard Library

1

Here is the documentation for the factorial function from the math module.

We can try it out:

Python
>>> import math
>>> math.factorial(5)
120
>>> math.factorial(5.0)
120
>>> math.factorial(-4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: factorial() not defined for negative values

How does our function differ? Here we have picked the improved factorial function from the questions to chapter 2:

Python
>>> def factorial(x):
...     if x < 0:
...         return 0
...     elif x == 0:
...         return 1
...     else:
...         return x * factorial(x - 1)
>>> factorial(5)
120
>>> factorial(5.0)
120.0
>>> factorial(-4)
0

We return a floating-point number for a floating-point input, unlike math.factorial, and we return zero for a negative input, where math.factorial raises a ValueError exception.

2

We assume the string does represent an integer, then check each potential digit. If it is not in the string string.digits, we unset the is_integer variable. We then return the variable as the result of the function.

There is one small problem: string_is_integer with the empty string will return True. Can you fix this?

3

This is a simple modification: we replace the use of random.randint with one of getpass.getpass, passing the prompt as an argument.

4

This is simple. We return them as a tuple.

What happens when there is no modal value in a list?

5

We use two functions: time.sleep, which does nothing for a given number of seconds, allowing us to give the user a count-down; and time.time which returns a floating-point value representing the number of seconds since an arbitrary point in the past. By measuring the time twice, and subtracting, we get the elapsed time.

What happens if the user presses Enter too soon? Can you fix this?

## 12. Building Bigger Programs

1

The guessing_game function is unaltered. We need simply to check that there are enough arguments in sys.argv. If there are, we pass the string representing the maximum number to guessing_game:

If not, we use the default value of ’100’. We could, in fact, pass the number 100 instead of the string ’100’, since the int function does not care if it is passed something which is already an integer. This, however, would make the program as a whole more difficult to read. Better to keep our types consistent.

2

We first write the file draw.py with our existing plotter:

Now the main plot.py program can use import to access the plot function from the draw module, passing the fabricated function f built from the command line argument (one function can sit inside another):

And so we may write:

What errors might occur? The wrong number of arguments is handled in our program, but what if float fails?

3

We write three little functions to list, add, and remove notes:

The main part of the program, then, must decode the command line to decide which operation to do, and what parameters it needs. If the command line too short or malformed, we print a message.

## Project 1: Pretty Pictures

1

We remember square takes the length of the side of the square as an argument:

Now we can write many_squares which takes how many square to use for the star, and the length of the sides, and calls square repeatedly.

2

The poly function is unaltered. We define a new function many_poly which takes the number of sides, the number of polygons to draw, and the length of the side of each polygon, and calls poly repeatedly, turning between each one.

Here is the result of many_poly(7, 16, 100):