Lists: the basics

The Python list is a powerful data type with a lot of functionality, so we’ll cover it in several posts. Here we start with some of its basic properties.

First, it’s important to understand that Python supports four different built-in compound data types: lists, tuples, sets and dictionaries. Each of these has its own set of rules and peculiarities.

A list is an ordered, mutable collection of data elements. It allows duplicate elements, that is, you can have 2 or more elements of the same datum in a list.

By ordered, we mean that the items in a list are stored in a definite order, so that, if we access say the fourth element of the list, it will always be the same datum (unless we’ve modified the list in the meantime). It’s important to note that ‘ordered’ does not necessarily mean ‘sorted’, so, for example, numeric values in a list need not be in ascending order (although it is possible to sort the items in a list, but that’s a different operation, as we’ll see).

By mutable, we mean that the contents of a list can be changed, either by adding or removing elements, or by changing the data stored at an existing list position.

As Python is a dynamically typed language, the elements in a list need not all be of the same data type, so we can mix ints, floats, strings and even other lists as elements within a given list.

At this stage, it’s useful to play around with lists in the PowerShell, so start one up and type ‘python’ at the prompt to enter the Python interpreter.

A list is specified by typing its elements, separated by commas, between square brackets. For example:

x = [1,2,2,3,4,4,5]

If you now type x at the prompt, you’ll just get [1,2,2,3,4,4,5] as the response.

To access a particular element within a list, give the list’s name followed by the element’s position in square brackets. The elements of a list are indexed starting with 0 for the first item, so x[2] gives the third element. Typing x[2] after the above gives us 2.

We can use the same notation to change a list element. Try typing x[2] = 'wibble' followed by x. You’ll see the list is now [1, 2, 'wibble', 3, 4, 4, 5]. This illustrates that you can mix data types within a list.

Now try y = [x, 3.14, -7+4j], and then type y to see what y looks like. You’ll see that it is [[1, 2, 'wibble', 3, 4, 4, 5], 3.14, (-7+4j)]. The list x is now the first element of y, followed by the float 3.14 and the complex number -7+4j.

If you want a single list consisting of the elements of x followed by 3.14 and -7+4j, you can use the + operator: z = x + [3.14, -7+4j]. If you print out z, you’ll see that it is [1, 2, 'wibble', 3, 4, 4, 5, 3.14, (-7+4j)]. Note that the inner pair of brackets around the first 7 elements is now absent, so all the primitive data elements are part of a single list. If you want to append a list to an existing list, you can use the += operator, as in x += [3.14, -7+4j].

The number of elements in a list x can be obtained using len(x).

Exercises

1. Write a program that generates 10 random floats between 0 and 1 and stores them in a list. Given that, for a list x, the statement x.sort() sorts the list into ascending order, print out both the original list and the sorted list. You’ll need the random() function from the module random to generate the random numbers.

See answer
from random import *
ranList = []
for i in range(10):
    ranList += [random()]
print('Unsorted list:')
print(ranList)
ranList.sort()
print('Sorted list:')
print(ranList)

2. You’ll see from the above exercise that running the sort() function on a list replaces the list with its sorted version. If you want to retain the original list as well as have a sorted version of the list, you’ll need to copy the list before you sort it. Investigate the copy() function for a list, and modify the above program so that both the original list and its sorted version are present at the end (so you can print them both out after doing the sort).

See answer
from random import *
ranList = []
for i in range(10):
    ranList += [random()]
ranCopy = ranList.copy()
ranList.sort()
print('Unsorted list:')
print(ranCopy)
print('Sorted list:')
print(ranList)

Accessing and changing list elements

As mentioned above, a single list element can be accessed or changed by specifying its index, as in x[2]. We can also use a negative index to count backwards from the end of the list, so x[-2] is the second element from the end. When counting backwards, the last element of the list has index -1, the second last by -2 and so on. When counting forwards, the index starts at 0.

We can also specify a range of indexes from within a list. The notation z[m:n] returns a new list containing elements m…n-1 from the original list. With the list z = [1, 2, 'wibble', 3, 4, 4, 5, 3.14, (-7+4j)], z[1:3] is the list [2, 'wibble'] (that is, the elements 1 and 2 from the original list). It’s often confusing that the index n in z[m:n] is not the index of the last element returned; rather it is the index of the element after the last element returned.

This sublist notation has a couple of shorthand versions. The list z[:3] returns elements 0 through 2, z[3:] returns elements 3 through to the end of the list, and z[:] returns a copy of the entire list (so this is an alternative to the copy() function for copying a list).

You can use negative indexes in sublist expressions, so that z[-3:-1] returns sublist starting with the element third from the end and ending with the element just before the last element (remember that the last element has index -1, and the sublist notation returns elements up to, but not including, the element with the second index).

If you give an impossible combination of indexes, such as z[4:2], you just get back an empty list (rather than an error message, so be careful!). Also, if one of the indexes in a sublist is out of range (that is, no such element exists) you’ll still get a valid list returned, rather than an error. Specifying a single list element that is out of range, however, will give you an error. It can all be somewhat confusing, so just take care!

Lists and for loops

A list can serve as a source of elements processed by a for loop. For example:

z = [1, 2, 'wibble', 3, 4, 4, 5, 3.14, (-7+4j)]
for elem in z:
    print(type(elem))

This program prints the data type of each element in the list, so we get

<class ‘int’>
<class ‘int’>
<class ‘str’>
<class ‘int’>
<class ‘int’>
<class ‘int’>
<class ‘int’>
<class ‘float’>
<class ‘complex’>

Exercise

Write a program that asks the user for an integer numRands, then generates a list containing numRands random numbers between 0 and 1. You should sort this list (no need to retain a copy of the original list). You should then save the portion of the list with numbers > 0.5 in a new list, print out the length of this list, and then calculate the average value of all the numbers in that list. If the random number generator is working properly, you’d expect the length of the sublist to be about half of numRands, and the average value of numbers > 0.5 to be around 0.75.

There’s no need to check the user’s input is valid (although you can if you want to). You should include tests to handle the cases where either all the random numbers are < 0.5 or > 0.5.

See answer
from random import *
while True:
    numRands = input('How many random numbers (or \'quit\')? ')
    if numRands == 'quit':
        break

    numRands = int(numRands)        # Number of random numbers to test
    ranList = []
    for i in range(numRands):       # Generate the list of randoms
        ranList += [random()]
    ranList.sort()                  # Sort it
    count = 0
    ranLength = len(ranList)
    while count < ranLength and ranList[count] < 0.5:     # Find the first element > 0.5
        count += 1
    upperHalf = ranList[count:]     # Create the sublist of elements > 0.5
    print('Number > 0.5: ', len(upperHalf))
    if len(upperHalf) > 0:
        average = 0.0
        for num in upperHalf:           # Calculate the average of the sublist
            average += num
        average /= len(upperHalf)
        print('Average of upper half =', average)
    else:
        print('No elements > 0.5')


Hopefully, the comments explain what the code is doing. The condition count < ranLength on line 14 checks for the case where all random numbers are < 0.5 (which can happen if numRands is small), and the test on line 18 checks for the case where all the numbers are < 0.5 (so the sublist is empty), which again can happen if numRands is small.

For loops and the range() function

In addition to the while loop, Python also provides the for loop. Its syntax has the form:

for [variable] in [collection of values]:
    do something for each member of the collection
    do more...
else:
    do something after all values in the collection have been processed

The [variable] is applied successively to each datum in the [collection of values] and the code in the first block is run in each case. After all the data in the collection have been processed, control passes to the else block (if present; it’s optional). Note that ‘in’ is a keyword in Python, and cannot be used as a variable name.

This is a rare instance in which a variable does not need to be initialized before it is used. The [variable] in the for statement is initialized by setting it to the first datum in the [collection of values].

Like the while loop, a for loop can be terminated at any point by using the break statement. Also as with the while loop, a continue statement causes the remainder of the current iteration to be skipped, with control passing back to the top line of the loop, where the next element of the collection is then processed.

The [collection of values] can be any of Python’s collective data types, including lists, tuples, sets and dictionaries. We’ll examine these data types later, although their use with for loops is straightforward: the loop just processes each element in the data type, as you’d expect.

The range() function

To illustrate for loops, we’ll use the range() function, as this gives a behaviour closest to what programmers in languages such as C++, C# and Java are used to. It has the syntax:

# Integers from 0 to stop - 1
range(stop)
# Integers from start to stop - 1
range(start, stop)
# Integers from start to stop - 1 in intervals of step
range(start, stop, step)

The first form generates an iterable object containing integers from 0 up to stop - 1, in intervals of 1. The second form does the same, but starts at ‘start’ rather than 0. The last form generates integers from start to stop - 1 with an interval of step between each pair of integers. The first two forms are just shorthand for the third, so we have range(stop) == range(0, stop) == range(0, stop, 1) and range(start, stop) == range(start, stop, 1).

range() generates an object which can be accessed like an array of integers, so we can specify an index to access a particular element. For example, enter the following in the console:

ran = range(3, 20, 2) 
ran[3]

The range array consists of the numbers [3, 5, 7, 9, 11, 13, 15, 17, 19], so ran[3] has the value 9. The sequence always ends with the largest number that is strictly less than stop.

range() can also generate values in decreasing order if the step is a negative integer. In this case, start must be greater than stop, otherwise range() will give an empty array. Thus range(10, 2, -1) gives the array [10, 9, 8, 7, 6, 5, 4, 3]. In the decreasing sequence case, the sequence ends with the smallest number that is strictly greater than stop.

If you want to see the complete array generated by range(), you need to convert it to a list. For example, typing

range(3, 20, 2)

into the console just produces range(3, 20, 2) as output. However, if you try this:

ranlist = list(range(3, 20, 2))
ranlist

you’ll get the list [3, 5, 7, 9, 11, 13, 15, 17, 19] printed out.

Using range() with for loops

The range() function can be used to provide the [collection of values] in the for loop syntax above. There is no need to convert range() to a list in order to use it in a for loop; the for loop is intelligent enough to iterate through the values in the range() object on its own.

Here’s an example that prints out a list of cubes for integers in a given range, and also illustrates another use of a for loop:

print('Cube table')
limits = input('Enter start, stop, step: ')
bounds = [int(i) for i in limits.split()]
for num in range(bounds[0], bounds[1] + 1, bounds[2]):
    print(str(num) + ' ** 3 = ' + str(num **3))
else:
    print('List finished.')

Line 2 reads in start, stop and step as a single string, so we need to split this string into 3 separate values and convert them to ints. There are several ways this can be done, but probably the most concise is that shown in the example, although it uses a technique called list comprehension which we will cover in more detail when we examine lists. Basically, line 3 first uses the split() function in the string library to split the limits string. By default, split() splits a string at each occurrence of whitespace and returns a list of the separate pieces. These pieces are still strings, so we need to convert them to ints before they can be used in the range() function. The statement on line 3 uses a for loop to iterate over the elements in the split list and applies int(i) to each such element. By enclosing the statement in square brackets, the result of this operation is returned as a list, which is referenced by the variable bounds.

Once the bounds list has been constructed, we use it in line 4 to provide the arguments for the range() function and calculate the table of cubes. When the loop finishes, control passes to the else block for the final message.

Exercise

Modify the cubes program above as follows:

(a) The user may input 1, 2 or 3 values rather than always 3. If the user enters a single number, print out a table of cubes for numbers from 1 up to that number, in steps of 1. If the user enters 2 numbers, calculate the table of cubes from the first number up to (and including) the second, in steps of 1. If the user enters 3 positive numbers, the program should function as before.

(b) The program should generate a table in descending order if the step variable is negative. This should occur only if the user enters 3 numbers with the last one being negative.

(c) Replace the list comprehension (line 4 above) with an ordinary for loop that converts the numbers entered by the user from strings to ints.

(d) The program should loop continuously allowing the user to generate as many tables as desired. Entering ‘quit’ should terminate the program.

(e) Check that step is not zero, since passing a zero step size into range() causes an error. If step is zero, skip the rest of that iteration and ask the user to try again.

See answer
print('Cube table')
while True:
    limits = input('Enter 1, 2 or 3 ints (\'quit\' to exit): ')
    if limits == 'quit':
        break
    limits = limits.split()     # Split into separate strings
    numLimits = len(limits)     # Find the number of ints entered

    for i in range(numLimits):  # Convert strings to ints
        limits[i] = int(limits[i])

    bounds = [1, 1, 1]          # Initialize the bounds list

    if numLimits == 1:          # Determine start, stop and step
        bounds[1] = limits[0]
    elif numLimits == 2:
        bounds[0] = limits[0]
        bounds[1] = limits[1]
    else:
        bounds[0] = limits[0]
        bounds[1] = limits[1]
        bounds[2] = limits[2]
        if bounds[2] == 0:     # If step is 0, skip this iteration
            continue

    if bounds[2] < 0:          # Adjust stop so that a full list is printed
        stop = bounds[1] - 1
    else:
        stop = bounds[1] + 1

    for num in range(bounds[0], stop, bounds[2]):
        print(str(num) + ' ** 3 = ' + str(num **3))
    else:
        print('List finished.')

Most of the code should be self-explanatory with the comments, but here’s a summary.

The while loop on line 2 allows the user to enter as many data sets as desired. If ‘quit’ is input, line 5 breaks out of the while loop and ends the program. Line 6 splits the input into separate strings and line 7 determines how many numbers the user entered. Lines 9 and 10 convert these into ints.

Lines 14 through 22 sort out the values of start, stop and step that will be used in the call to range() in line 31. If the user entered only 1 number, then the table should go from 1 to that number, in increments of 1, so only the value of stop (bounds[1]) needs to be set. If 2 numbers were entered, the first is start (bounds[0]) and the second is stop (bounds[1]), while if 3 numbers were entered, we set the values of start, stop and step.

Line 23 checks that the step size is non-zero, and line 24 skips the rest of the iteration if it is.

Lines 26 through 29 adjust the value of stop to take account of the fact that the second argument in range() is non-inclusive, so we need to subtract 1 if we want a descending sequence or add 1 if we want an ascending sequence. Finally, lines 31 and 32 print out the table as before.

While loops

Python has two basic loops: while and for. Of these two, while is probably the simpler, so we’ll start with a look at it.

The basic syntax of a while loop is:

while [condition is True]:
    do something
    do something else
else:
    do something when condition is False

The ‘condition’ in the first line is a boolean expression such as those we studied earlier. As long as this condition is True, the statements in the first block will be executed. If the ‘condition’ is False, the ‘else’ clause is activated, and any statements within this block will be run. The ‘else’ block is optional, and is a feature not found in most other traditional languages.

The loop can be ended with a break statement anywhere within its block. This transfers control to the first statement after the entire loop (not to the else block within the loop).

A continue statement anywhere within the loop causes the rest of the code in that iteration of the loop to be skipped, and control passes back to the opening while statement. Note that continue does not exit the loop; it merely skips one iteration within the loop. If you want to exit the loop entirely, use break.

Here’s an example:

word = 'go'
while (word != 'stop'):
    word = input('Enter some text: ')
    if word.isdigit():
        print('You\'ve entered an integer:', word)
        continue
    elif word.isupper():
        print('The string is UPPERCASE:', word)
    elif word.islower():
        print('The string is lowercase:', word)
    else:         # Mixed upper and lowercase
        break
    word = word.lower()
else:
    print('Quitting the program.')

After initializing word on line 1 (remember all variables in Python must be initialized before they can be used), we enter the loop. The loop’s condition is True as long as word is not ‘stop’. We ask the user to enter some text, then test if the user entered only digits. If so, the continue on line 6 skips the rest of the loop (including the else on line 14) and returns control back to the start of the loop on line 2.

If the user entered text that is entirely uppercase, we print the message on line 8; if it’s all lowercase, we print the message on line 10. If the entered text is none of: integer, all upper or all lowercase, the break on line 12 causes the loop to be exited immediately, so the program ends (without executing the else clause, so the message ‘Quitting the program’ is not printed).

If the user enters the string ‘stop’ (in either all upper or all lowercase, but not mixed), the condition in line 2 is False, and the else clause in line 14 will run, after which the program ends.

If the program reaches line 13 (which will happen only if the continue in line 6 and the break in line 12 are not reached), word is converted to lowercase so it can be compared with ‘stop’ in line 2.

Exercise

Write a program that asks the user to enter a positive integer. The program should check that the entered datum is a positive integer, and quit if anything else is entered. For a valid input, the program should then calculate the factorial of that integer and print it out, followed by a prompt for the user to enter another integer. The program should quit if the user enters anything other than a positive integer.

As a reminder, the factorial of an integer n is defined as n!\equiv n\times (n-1) \times (n-2) \times \ldots \times 2 \times 1. You should use the while loop as the only loop structure in this program.

See answer
num = 1
while num > 0:
    num = input('Enter a positive integer: ')
    if not num.isdigit() or int(num) < 1:
        break
    num = int(num)
    fac = num
    while num > 1:
        fac *= num - 1
        num -= 1
    else:
        print(fac)

We use two nested while loops. The outer loop reads the input and tests that the user has entered a positive integer (line 4). If not, the outer loop breaks on line 5, and since nothing follows the outer loop, the program quits.

If num is a positive integer, it is converted to an int on line 6 (remember that all input is read as strings). The variable fac is initialized to num and successively multiplied by a decreasing sequence of integers in the inner while loop. When num is reduced to 1, the condition in the inner loop becomes False, and its else clause is run, which prints out the factorial num!. Note that at this point, the value of num is 1 (not 0) so when control returns to the outer while loop, the condition num > 0 is True, and the loop continues with another request for input.

As a side note, since Python has no limit (other than computer memory) on the size of integer it can represent, you can get the factorial of some quite large numbers. Pocket calculators are usually restricted to numbers with a couple of hundred digits, but you can get a lot higher in this program. For example, try calculating 10000! (Be prepared to do a lot scrolling in the output window to see the full answer.) Try modifying the above program to print out the number of digits in n! rather than the factorial itself. Hint: use the len() function. You will find that 10000! has 35660 digits.

Installing Python packages in Visual Studio

Although Visual Studio 2019 comes with a complete basic Python installation, there are many third-party packages that you may wish to use, so it’s useful to know how to install these. Here we’ll give a couple of examples. First, we’ll show how to install matplotlib, which is a package that shows plots of mathematical functions.

Installing a new Python package

Create a new Python project in Visual Studio. Enter the following code into the main code window (usually at the upper left):

from math import radians
import numpy as np     # installed with matplotlib
import matplotlib.pyplot as plt

def main():
    x = np.arange(0.01, radians(1800), radians(12))
    plt.plot(x, np.sin(x)/x, 'Goldenrod')
    plt.show()

main()

Don’t worry about the details of the code for now; we’ll cover this in future posts. This code should show a plot of the function \frac{\sin {x}}{x} for x between 0.01 (to avoid dividing by zero) up to 1800 degrees, with a spacing between plot points of 12 degrees . The final argument in the np.arange() function specifies the colour of the plot, using one of the standard HTML colour names.

If you don’t have matplotlib installed, the editor will underline the terms numpy, matplotlib and pyplot in lines 2 and 3 and complain that they are unknown. To install them, follow these steps:

In Visual Studio’s View menu, select View>Other windows>Python environments. This should open a panel on the right that looks like this:

Don’t worry if your panel doesn’t look exactly like this; the Python environments listed depend on how many you have installed. If you installed Visual Studio with just the default Python environment, you’ll probably have only Python 3.7 installed.

Next, click on the drop down menu labelled Overview. You should see an option called ‘Packages’, as shown:

Select this, and you should then get a box with the text ‘Search PyPl and installed packages’. Type ‘matplotlib’ into this box and you should see a list of options containing this term. Click on ‘Install matplotlib’. You should then see the Package Manager Console giving reports of progress in the installation process (which might take a couple of minutes so be patient).

When it’s done, you can then run the above program and, hopefully, see this output:

Installing a different version of Python

One of the annoying things about Python is that different versions support different features and, sometimes, actually break code that was written in an earlier version. If you want to test your code in a different version than the one in which it was written, you can install a different version of Python in Visual Studio.

To do this, click on ‘Add Environment…’ in the Python Environments panel shown above. In the dialog box that appears, select ‘Python installation’ on the left, and you’ll see a list of available Python versions that you can install, along with those that are already installed. Select one or more of these versions and then click the ‘Install’ button at the lower right. You’ll need administrator privileges to do this.

Let’s say you’ve decided to install Python version 2.7 alongside the default Python 3.7 and you want to test your program using 2.7. On Visual Studio’s Project menu, select Project>[Project name] properties, where [Project name] is whatever you’ve called your project. You’ll then get window with tabs named ‘General’, ‘Debug’, ‘Publish’ and ‘Test’ on the left. On the General tab, select the Python interpreter you want from the drop down menu at the bottom. Make sure you save the change before running your program. (Click Ctrl+S or the Save icon in the toolbar.) You can then run your project to see what happens.

If you try this on the plotting program above, you’ll probably get an error complaining that matplotlib doesn’t exist. This is because installing a Python package applies only to one version of Python, so above we installed matplotlib for Python 3.7 but not for 2.7. If you want your program to work with 2.7, you’ll need to repeat the above steps to install matplotlib for 2.7 as well.

Comparisons and the if statement

The if statement

For making choices among several alternatives, Python, like most languages, provides the ‘if’ statement. The ‘if’ statement is a compound statement, so Python’s indentation rules apply. The general format of ‘if’ is as shown:

if some condition: # if 'some condition' is True
    do something
elif another condition:  # else if another condition is True
    do something else
elif yet another condition: # else if yet another condition is True
    do something else again
...            # as many more elif statements as you like
else:  # if none of the above conditions is True
    do some default action

The ‘conditions’ referred to must be objects that have a True or False value, but in Python, this can be almost anything. Numeric objects are False if they are zero and True otherwise; strings are False if they are empty and True otherwise, and so on.

The bool() function

To determine the boolean value of an object, use the bool() function. Try typing the following into a Python console:

bool(0)
bool(1)
bool('')
bool('wibble')
bool(0 + 0j)
bool(0 + 1j)
bool(0 * 42)
bool(12 - 12)
bool(12 + 12)
bool(0.1 + 0.2 - 0.3)

The last example returns True (even though the result should be 0), which is a result of the infamous roundoff error present when using floats.

Comparison operators

The usual comparison operators are available in Python:

== equal to
!= not equal to
< less than
<= less than or equal to
> greater than
>= greater than or equal to

Comparisons between numeric types work as you’d expect. Comparisons between strings use alphabetical order to determine the result of <, <=, >, >= operations. This uses the ASCII ordering of characters, so uppercase letters are all before lowercase letters. Thus ‘Zebra’ < ‘aardvark’ is True, while ‘zebra’ < ‘aardvark’ is False. If you want a case-insensitive comparison, use str.upper() to convert strings to uppercase, or str.lower() to convert to lowercase.

Although you can’t use <, <=, >, >= operations with mixed data types (for example, ‘wibble’ > 42 gives an error), you can use == and != to compare any two data types. Not surprisingly == is always False if the data types don’t match. For example 0 == ” is False, even though you’re comparing zero to an empty string.

Unlike in many other languages, comparisons can be chained in Python, so expressions such as 1 < 2 < 4 < 12 (True) and 1 < 2 > 4 < 12 (False) are valid. A chained expression is True only if all its components (evaluated from left to right) are True.

Boolean operators

Although the comparison operators do much what you’d expect, the boolean operators ‘and’ and ‘or’  are not entirely intuitive.

If you use them to compare the results of two comparisons, you’d get what you’d expect. For example, 1 < 2 and 2 + 4 == 6 returns True. It’s important to note that the boolean operators have a lower precedence than the comparison operators, which in turn have a lower precedence than arithmetic operators. In the expression 1 < 2 and 2 + 4 == 6, 2 + 4 is done first, then the comparisons 1 < 2 and 2 + 4 == 6 (in that order, left to right) and finally ‘and’ is applied to the results.

The catch is that boolean operators can be applied to any objects, not just to the results of comparisons. Consider the ‘and’ operator applied to two objects. A boolean ‘and’ is True only if both its operands are true, so we might expect 42 and 19 to return simply True (try it!). In fact, it returns the value 19, rather than just True.

The algorithm Python uses when confronted with an ‘and’ of two objects is to start with the left hand object and test whether it’s True or False. If it’s True, it then just returns the right hand object, regardless of whether it is True or False. If the left hand object is False, however, it gets returned immediately and the right hand object is ignored. Since the only way the ‘and’ of two objects is True is if both operands are True, taking the bool() of the result always gives the right answer, but you need to be aware that the actual result of ‘and’ is not necessarily a boolean value.

For the ‘or’ operator, both operands are always evaluated. If only one of the operands is True, that one is returned. If both are True, the right hand one is returned. If both operands are False, the right hand one is returned.

The important thing to remember is that, unless the operands of ‘and’ and ‘or’ are themselves boolean quantities (False or True), the value returned will be an actual object, and not just a boolean quantity.

The ‘not’ operator, however, just returns False or True, and never any other quantity.

You should experiment with various data types to see how they behave with the comparison and boolean operators. In particular, take note of any combinations of data types that give errors.

Exercise

Use your favourite search engine to read about Python’s datetime module. Using this module, write a program that does the following:

  1. Determine the current time and print a welcome message that depends on the hour of the day. Print ‘Good morning’ if the time is between midnight and noon, ‘Good afternoon’ if the time is between noon and 6 PM, and ‘Good evening’ if the time is between 6 PM and midnight.
  2. Determine the day of the week and print out a message ‘Part of the working week’ if the day is Monday through Friday, or ‘It’s the weekend!’ if the day is Saturday or Sunday.
  3. Construct a datetime object for 31 December of the current year, and calculate how many days there are between now and the end of the year.
See answer
from datetime import *
current = datetime.now()
hour = current.hour
if 0 <= hour <= 11:
    print('Good morning.')
elif 12 <= hour < 18:
    print('Good afternoon')
else:
    print('Good evening')
dayOfWeek = current.weekday()
if dayOfWeek != 6 and dayOfWeek != 7:
    print('Part of the working week')
else:
    print('It\'s the weekend!')
endOfYear = datetime(current.year, 12, 31)
daysTillEnd = endOfYear - current
print('Days until the end of', current.year, '=', daysTillEnd.days)

Line 2 uses the now() function to get the current date and time. Line 10 uses the weekday() function to find the day of the week, with Monday = 0 and Sunday = 7. Line 15 constructs a new datetime object for 31 December of the current year. On line 16, note that subtraction is defined for datetime objects, so we can use this to obtain the number of days between two dates.

Indentation

If you are to use Python for anything beyond simple one- or two-line programs, you’ll need to use some form of compound statement, such as a conditional (if) statement or some form of loop. A compound statement usually consists of an opening clause (such as if x < y) followed by one or more statements that are to be run as part of the compound statement.

If you’re used to other languages such as C++, C# or Java, you’ll also be used to the syntax these languages require for compound statements. If you were taught these languages properly (either by self-study from a book or from a good teacher), you will have been taught to indent each line within a compound statement to improve the readability of your code. However, in these languages, indentation is optional, in the sense that, if you didn’t indent your code at all, or used haphazard indentation where each line was indented by a different amount, it would make no difference to the correctness of the code. If it was logically and syntactically correct, it would still run and give you the correct results. [However, a good teacher would still dock marks if you didn’t indent your code, so it makes code difficult to understand and maintain.]

Python promotes indentation from ‘just a good idea’ to ‘essential’. Statements within a compound statement must be indented relative to the opening line of the compound statement, and the indentation must be consistent: that is, each line must be indented by the same amount.

Although we haven’t yet studied conditional statements or loops in detail, we can use a few simple examples of these statements to illustrate Python’s indentation requirements.

Before I present the code, I will make a recommendation about project management. Most of the code we’ll study from now on will involve programs that span several lines, so it can get tedious and repetitive if you try to enter these programs into an interactive Python console. If you’re using Visual Studio to run your Python examples, I’d strongly recommend that you create a new Python project and enter your code there, rather than use the interactive console.

Right, now on to our first complete program. The following code asks the user to input an integer. If the user types ‘stop’ instead, the program quits; otherwise, it tests to see if the user has entered a valid integer, and then prints out a string showing the multiplication table (up to 12) for that integer.

while True:
    num = input('Enter an integer (or \'stop\' to quit): ')
    if num == 'stop':
        break
    if not num.isdigit():
        print('Not an integer. Try again.')
    else:
        num = int(num)
        table = ''
        for i in range(13):
            table += str(num * i) + ' '
        print(table)

We’ll leave a full discussion of the if statement and the while and for loops until later, but it’s worth noting a few points here.

First (and very important): in Python, the opening line of any compound statement must end with a colon (:). This is probably the biggest bugbear that will catch out programmers used to languages like C++, C# and Java.

Second, the conditions in ‘if’ statements and loops need not be enclosed in parentheses (although they can be if you really insist on it).

Now, as to the code itself:

Line 1 creates an infinite loop, since the condition for the while loop is given as True, which obviously is always, well, true. This isn’t exactly best programming practice, but in this case it’s OK since we provide a way for the user to exit the loop (by typing ‘stop’ at the input prompt). The input() function reads input from the console and returns the result as a string.

The ‘break’ statement on line 4 breaks out of the current level of loop. In this case, at the point where ‘break’ occurs, we are within only the top-level while loop, so the program breaks out of this loop.

Line 5 checks to see if the user entered a valid integer by using the isdigit() function, which is part of the string library. It checks that the string contains at least one character, and that all the characters are integers. Thus it will return false if the user enters a float (with a decimal point) or any string containing letters.

If the input passes the test (that is, it’s an integer), line 8 converts it to an integer (remember that ‘input’ returns the input as a string, and we can’t perform arithmetic on strings). Line 9 initializes table to a string with no characters. Line 10 uses a for loop to iterate the value of i from 0 up to one less than the argument of range(13), so i ranges from 0 to 12.

Line 11 is part of the for loop, and extends table by attaching the result of num * i, converted to a string. Finally, line 12 prints out the result.

It’s important to note the indentation levels used here. The entire program is part of the opening while loop, so everything after line 1 must be indented relative to the opening while. The if statement on line 3 contains only a single statement, namely the ‘break’ on line 4, so line 4 must be indented relative to line 3. Line 5 opens a new ‘if’ statement, so again we must indent statements within it. The ‘else’ on line 7 has the same logical level as the ‘if’ on line 5, so it reverts to the same indentation level as line 5. All the remaining statements (lines 8 to 12) are part of the ‘else’, so they are all indented to the same level. Finally, line 11 is part of the ‘for’ loop, so must be indented relative to line 10.

Exercises

  1. Modify the above code by indenting line 12 so it has the same level as line 11. Try to predict the result before you run it. Is this valid Python code? What will be printed out?
See answers

Yes, it is valid Python code. It moves the final print(table) statement to be within the for loop, so ‘table’ is printed out after each multiplication, rather than just at the end. Sample output:

Enter an integer (or ‘stop’ to quit): 12
0
0 12
0 12 24
0 12 24 36
0 12 24 36 48
0 12 24 36 48 60
0 12 24 36 48 60 72
0 12 24 36 48 60 72 84
0 12 24 36 48 60 72 84 96
0 12 24 36 48 60 72 84 96 108
0 12 24 36 48 60 72 84 96 108 120
0 12 24 36 48 60 72 84 96 108 120 132
0 12 24 36 48 60 72 84 96 108 120 132 144

2. Write a program that calculates and displays the first n Fibonacci numbers. The Fibonacci sequence starts with f_1=1, f_2=1 and then every succeeding number is the sum of previous two, so f_{n+1}=f_{n}+f_{n-1}. Your program should ask for an integer input, check if the user entered ‘stop’, then check that the input is a valid integer, then calculate the first n Fibonacci numbers and print them out on a single line.

See answers
while True:
    num = input('How many Fibonacci numbers (\'stop\' to quit)?: ')
    if num == 'stop':
        break
    if not num.isdigit():
        print('Not an integer. Try again.')
    else:
        fib1 = 1
        fib2 = 1
        num = int(num)
        table = str(fib1) + ' ' + str(fib2)
        for i in range(num - 2):
            fib3 = fib1 + fib2
            table += ' ' + str(fib3)
            fib1 = fib2
            fib2 = fib3
        print(table)

Typical output of this program is
How many Fibonacci numbers (‘stop’ to quit)?: 20
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765
How many Fibonacci numbers (‘stop’ to quit)?: stop
Press any key to continue . . .

Complex numbers

If your background includes mathematics or physics, you’ve no doubt encountered complex numbers. The basic unit used in complex numbers is the square root of -1, denoted in mathematics and physics books by a lowercase i, and in engineering by j. Any number that is a multiple of i is called an imaginary number. A complex number is the sum of a real number and an imaginary number, as in 3+4i or -7.2-9.3i. [As my background is in physics, I tend to prefer i over j, but we’ll have to overcome that predilection when we program in Python, as we’ll see.]

Python uses an upper or lowercase ‘J’ or ‘j’ to denote \sqrt{-1}. Thus we can define the above complex numbers in Python as:

z1 = 3 + 4j
z2 = -7.2 - 9.3j

If the real or imaginary part of a complex number contains a decimal point, it is stored as a float, so the usual problems with roundoff error can occur with complex numbers as well. Note that if you want to use the quantity j on its own, you must prefix it with 1, as in z = 1j. The letter ‘j’ on its own is interpreted as a variable name.

All the basic arithmetic operations are defined for complex numbers. As a reminder, the arithmetic operations are defined as follows for complex numbers z_1=a+bj and z_2=c+dj:

    \begin{align*} z_{1}+z_{2} & =\left(a+c\right)+\left(b+d\right)j\\ z_{1}-z_{2} & =\left(a-c\right)+\left(b-d\right)j\\ z_{1}\times z_{2} & =\left(a+bj\right)\left(c+dj\right)\\ & =ac-bd+\left(ad+bc\right)j\\ \frac{z_{1}}{z_{2}} & =\frac{a+bj}{c+dj}\\ & =\frac{\left(a+bj\right)\left(c-dj\right)}{\left(c+dj\right)\left(c-dj\right)}\\ & =\frac{\left(ac+bd\right)+\left(bc-ad\right)j}{c^{2}+d^{2}} \end{align*}

Try these to see what you get:

z1 + z2
z1 - z2
z1 * z2
z1 / z2
See answers
(-4.2-5.300000000000001j)
(10.2+13.3j)
(15.600000000000001-56.7j)
(-0.42507048362611144-0.0065061808718282306j)

Note that the integer division operator // is not defined for complex numbers.

To do more advanced operations on complex numbers, you’ll need the cmath module. This allows you to express a complex number z in polar form as z=re^{j\theta}, where r is the modulus or amplitude of z (written |z|=r) and \theta is the phase. You can also apply standard functions such as logarithms, trigonometric and hyperbolic functions and their inverses. Here are a few examples to try:

from cmath import *
z = 3 + 2j
polar(z)
abs(z)
phase(z)
log(z)
See answers
(3.605551275463989, 0.5880026035475675)
3.605551275463989
0.5880026035475675
(1.2824746787307684+0.5880026035475675j)

The polar(z) function converts z to polar form, which returns a tuple, with the first element being the amplitude of z and the second argument the phase. The abs(z) function returns the amplitude (or absolute value) of z and phase(z) returns the phase (in radians).

The log(z) function returns the log (to base e). Since z=re^{j\theta}, applying the usual rules for the logarithm of a product and an exponential we get \log\left(re^{j\theta}\right)=\log r+j\theta. We see from the answer above that log(3.605551275463989) = 1.2824746787307684, and the imaginary part is just j times the phase, as expected.

Readers familiar with complex variable theory will realize that the logarithm of a complex number is not unique. Since e^{j\theta}=e^{j\left(\theta+2n\pi\right)} for any integer n, we can in principle add any multiple of 2\pi to the imaginary part of any logarithm. The log function in cmath always returns a value with a phase in the range -\pi<\theta\le\pi. If you want a different branch of the logarithm, you’ll need to account for it in the surrounding code.

Similar problems arise when calculating roots of complex numbers. If we calculate the square root of a positive real number, there are two possible answers, differing in the sign. In general, when calculating the nth root of a complex number, there are n different possible results. The answer returned by Python is the one obtained from taking the nth root of the form of the complex number with the smallest phase. That is, if z=re^{j\theta}, then if we calculate z ** (1/n), we get z^{\left(1/n\right)}=r^{\left(1/n\right)}e^{j\theta/n}. Other roots would have to be calculated from this one.

Other functions of complex numbers have similar peculiarities, so you’ll need to be familiar with the mathematics if you want to use them properly. I don’t want to get too bogged down with details, but perhaps one more example might be useful.

To calculate the cosine of a complex number z we can use the formula \cos z=\frac{e^{jz}+e^{-jz}}{2}. For z=3+2j, for example, this formula gives us

    \begin{align*} \cos\left(3+2j\right) & =\frac{1}{2}\left(e^{j\left(3+2j\right)}+e^{-j\left(3+2j\right)}\right)\\ & =\frac{1}{2}\left(e^{-2}e^{3j}+e^{2}e^{-3j}\right) \end{align*}

If you enter this formula into the Python console and compare the result with cos(z), you’ll see that they are the same (well, up to the penultimate digit, anyway).

To get a complete list of functions in cmath, in a fresh console type the commands:

import cmath
help(cmath)

Exercises

Write Python code to calculate the three cube roots (one real, two complex) of +1. Verify your answer by cubing the results and showing that they do indeed give +1.

See answers
The integer +1 can be written as e^{0}, e^{2\pi j} and e^{4\pi j}, so its three cube roots are +1, e^{2\pi j/3} and e^{4\pi j/3}.

Code to calculate these three roots is:

from cmath import *
z1 = 1
z2 = exp(2j * pi / 3)
z3 = z2 * z2
# Verify the results
z1 ** 3
z2 ** 3
z3 ** 3

When you calculated the cubes in the above exercise, you found that the answers were only approximately equal to +1 due to roundoff error. Try to find a way of expressing the cubes so that they give exactly +1.

See answers

You can modify the code as follows:

from cmath import *
z1 = 1
z2 = exp(2j * pi / 3)
z3 = z2 * z2
# Verify the results
z1cube = z1 ** 3
z2cube = z2 ** 3
z3cube = z3 ** 3
# Use the round() function on real & imaginary parts separately
z1cube = round(z1cube.real) + round(z1cube.imag) * 1j
z2cube = round(z2cube.real) + round(z2cube.imag) * 1j
z3cube = round(z3cube.real) + round(z3cube.imag) * 1j

Dynamic typing

If you’re used to languages such as C++, C# or Java, you’ll know that in these languages, variables must be declared before they can be used. Declaring a variable in these languages means that its data type must be specified. This allows the program to allocate space in memory for the declared variable and, in some cases, initialize its value.

In Python, variables are never declared, and must be assigned a value before they can be used anywhere else. To use Python variables properly, you need to understand how they are handled.

A Python variable name such as x is used only as a label. In order for x to have any meaning, it must be attached to some object. An object in Python can be as simple as a single integer, or as complex as a user-defined class with multiple data fields. When a variable is assigned to a data object, the data object is created and the variable name is attached to that object. One way of thinking of it is to imagine the data object is constructed separately and placed in a box, and then the variable name is like a label that is attached to the box. The variable is just one way of referring to this independent object. The properties of the object are contained in the object itself (inside the box), so they don’t need to be stored with the variable name (the label attached to the box).

One effect of this method of data storage is that a variable name such as x can be removed from one object and attached to another. The new object need not be the same data type as the first object, since all the attributes of the object are stored within the object and not with the variable name used to refer to it. In the box analogy, we can peel a label off one box and stick it onto another box, even if the boxes have completely different contents.

Let’s see how this works in practice. We’ll consider the following code:

x = 42
x
y = x
y
x = 51
x
y

In memory, this sequence of assignments looks like this:

 

 

 

The assignment x = 42 creates an integer object with the value 42 and attaches x as a label that refers to this object. Printing x after this assignment just echoes the value 42.

The assignment y = x is executed in two stages. First, the variable x on the right-hand side is used to locate the object to which it refers, which is the integer 42. Then y is attached as a label for this object. The result is that the integer object 42 now has two labels (x and y) attached to it. Note that there is no direct link between x and y; they are just independent labels that happen to refer to the same object. As a result, echoing x and then y after this assignment returns the value 42 in both cases.

Now consider the final assignment of x = 51. This creates a new integer object (independent of the original 42 object) and gives it the value 51. The label x is then removed from the 42 object and attached to the new 51 object. This has no effect on y, as it still refers to 42. Thus printing x gives 51 and printing y gives 42.

Note that we could have replaced the statement x = 51 with a statement like x = 'wibble', in which a string object with the value ‘wibble’ is created and then x is assigned as a label for this object. The same label x is allowed to refer to any data type, since the properties of the data type are always contained within the object, and not within the label applied to that object.

The same process applies to more complex expressions. The statement z = x + y, for example, first analyzes the RHS by looking up the objects referred to by x and y (which we’re assuming are integers), then adding them, then creating a new integer object with the sum and finally attaching z as the label for this new object.

At the level of simple expressions involving primitive data types, this is really all you need to know to understand dynamic typing. When we use more complex data types, however, you do need to be more careful. Although we haven’t yet examined lists, we can use a simple list here to illustrate the point.

A list in Python is just an ordered collection of objects, enclosed in square brackets. Consider the following code:

L = [42, 51, 60] 
M = L 
L[0] 
M[0] 
L[0] = 73 
L 
M 

This code is illustrated as follows.

The first line creates a list with 3 elements, and the label L is assigned to this list. The important point here is that each of the three elements within the list is itself a label which refers to the integer object stored at that point in the list. Lists are indexed with the initial element given the index 0, so the first list element is L[0], which here refers to the integer object 42.

The second line M = L first looks up the object to which L refers (which is the original list) and then assigns M as a second label for the same list. Note that this has no effect on the elements of the list; M is just a second label for the top level object, which is the list itself. We can therefore access the list’s elements using either label, L or M, thus L[0] and M[0] both print out 42.

Now suppose we give the command L[0] = 73. This creates a new integer object with the value 73 and assigns the label L[0] to refer to it. However, since this label is inside the list, the contents of the list are changed for both L and M. Thus if we print out the list using the L statement (on a line by itself) or M, we see that we get the same list values in both cases.

Finally, we consider this code:

L = [42, 51, 60] 
M = L[:] 
L 
M 
L[0] = 73 
L[0] 
M[0] 

In the first line, we create the same list as before and give it the label L. The second line M = L[:] is the syntax for copying a list (we’ll get to list syntax a bit later, so you can just accept this for now). If we print out L and M, we see we get what appears to be the same list. However, in this case, M refers to an independent copy of the list referred to by L, as in the diagram:

This time, if we change list L by the statement L[0] = 73, this affects only the list referred to by L and not the copy referred to by M. Thus L[0] now refers to the value 73 while M[0] still refers to 42.

References and garbage collection

If you read through the above carefully, one thing might bother you. In a series of statements such as

x = 42
x = 73
x = 61

we create an integer object with value 42, then another integer object with value 73 and finally another object with value 61. The same label x is successively applied to the three objects. After the final assignment, however, there is no label that refers to either the 42 or the 73, so what happens to them? As they were allocated memory but now have no references, do they just pile up until eventually the computer runs out of memory?

Fortunately not, as Python contains a built-in garbage collection system. Each object that is created contains a counter of how many labels (variable names) refer to that object. When the 42 object is created and x applied to it, the counter is incremented by 1. With the next assignment, a 73 object is created and its counter incremented, but since the label x was removed from the 42 object, its counter is decremented and returns to zero.

Whenever an object’s reference count becomes zero, Python automatically deletes it, thus freeing up its memory. If you’re curious about how many references an object has, you can use the getrefcount() function in the sys module:

from sys import *
getrefcount(M)
getrefcount(42)

In many cases, the count will probably be higher than you’d expect, as a lot of Python’s internal code creates and maintains objects that may be the same as those you define in your program. To save space, Python will often create a single instance of an integer with a given value, such as 42, and use this single instance in every place where the number 42 is needed. This doesn’t cause any problems, because integers are immutable, meaning that once they are created they can’t be changed. If a variable is to be assigned a new integer value, a new integer object is created and the variable assigned to it.

Fractions

If you have nightmares from trying to deal with fractions from your school days, Python’s Fraction data type may be just what you need. A Fraction is an object consisting of two integers, the numerator and denominator. To use it, you must import the fractions module. A basic Fraction can be constructed by passing it two integers as arguments. Try typing in the following into the console:

from fractions import *
Fraction(1, 3)
Fraction(5, 20)
Fraction(16, -64)
Fraction()
Fraction(13,0)

See answers


Fraction(1, 3)
Fraction(1, 4)
Fraction(-1, 4)
Fraction(0, 1)
ZeroDivisionError

The first line just returns a Fraction object equal to 1/3. The second line illustrates that Fraction automatically converts a fraction to its lowest terms: 5/20 = 1/4. The third line shows that a Fraction can be negative. The fourth line shows that the default values for the numerator and denominator are 0 and 1, respectively.

The final line shows that you can’t define a Fraction with a zero denominator, as you’d expect.

A Fraction can also be created from a float, although, as with Decimals, care is needed. If we naively write Fraction(33.24) for example, we get the result Fraction(4678114112931103, 140737488355328), which is probably not what you’d expect. However, if you divide 4678114112931103 by 140737488355328 you do, indeed, get 33.24. The problem, as usual with floats, is the roundoff error when trying to convert a float to binary form.

As with Decimals, we can get around this problem by passing a string version of a float to the Fraction constructor. If we try Fraction('33.24'), we get the result Fraction(831, 25). To figure out how Python arrived at this result, we look first at the fractional part of the argument, which is 0.24. In fractional form, this is 24/100, which reduces to 6/25. Thus the denominator will be 25. The numerator is calculated by converting the whole number part to 25ths and adding the 6: 33\times 25 + 6 = 831.

Another way of getting a sensible result for a Fraction from a float is to use the limit_denominator() function. Its argument specifies the largest integer that the denominator can be. With the example above, we can try

from fractions import *
Fraction(33.24).limit_denominator(2)
Fraction(33.24).limit_denominator(3)
Fraction(33.24).limit_denominator(25)
Fraction(33.24).limit_denominator(100)

See answers


Fraction(33, 1)
Fraction(100, 3)
Fraction(831, 25)
Fraction(831, 25)

Limiting the denominator to 2 produces the fraction 33/1 = 33, which is as good an approximation as we can get. Limiting the denominator to 3 gives us 100/3 = 33.333… Limiting the denominator to any integer from 25 up to 100 gives the exact answer of 831/25 = 33.24.

A Fraction can also be constructed from a Decimal. For example, we can write Fraction(Decimal('-97.5')) and get the result Fraction(-195, 2).

The numerator and denominator of a Fraction can be obtained by specifying the numerator or denominator property.

from fractions import *
x = Fraction(24, 37)
x.numerator
x.denominator

The third line returns 24 and the fourth line 37.

Fractions support the usual arithmetic operations, which makes them handy for adding up fractions with different denominators. For example, Fraction(1, 3) + Fraction(2, 5) produces Fraction(11, 15).

Functions from the math library can also be applied to Fractions, although in most cases the result will be a float, rather than another Fraction. For example:

from math import *
from fractions import *
x = Fraction(11, 15)
sqrt(x)
exp(x)
sin(x)

See answers


0.8563488385776752
2.0820090840784555
0.6693498402504662

Decimals

We’ve seen that floats in Python can sometimes be inaccurate due to roundoff error.  One way to correct this is to use Python’s Decimal data type. A Decimal variable allows the number of decimal points to be fixed, which can often eliminate roundoff error.

Decimals have a somewhat peculiar and clumsy syntax, however, so it’s worth looking at a few simple examples. The Decimal data type (with an uppercase ‘D’) is included in the decimal (with a lowercase ‘d’) module, so it must be imported before it can be used. [If you want to see the full contents of the decimal module, you can type help(decimal) in the console. However, be prepared for a very long list resulting from this. It’s probably more instructive to refer to the official documentation page on decimals.]

Intuitively, you might expect that simply feeding an ordinary float number into Decimal would convert it to a Decimal. To test this, try this in the console:

from decimal import *
Decimal(0.1)

The * in the first line imports everything from the decimal module.

See answer


Decimal(‘0.1000000000000000055511151231257827021181583404541015625’)

The result, despite the huge number of significant digits, is certainly not exactly 0.1. However, if we enclose the 0.1 in quotes, we get:

from decimal import *
Decimal('0.1')

Now we get Decimal('0.1') as the result. Enclosing a number in quotes converts it to a string, so we see that in order to create a Decimal from a float, we must enclose the float in quotes to produce a string.

When we first import the decimal module, we can see its default context by calling getcontext(). The result is

Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])

The ‘prec’ is the precision, or number of decimal places, which defaults to 28.

An obvious application of Decimals is in keeping track of quantities of money, which are usually in decimal form, with 100 of some smaller unit, such as pence or cents, equal to a larger unit, such as a British pound or dollar. You might think that setting the ‘prec’ to 2 would allow precise calculations with money, but in fact, it doesn’t.

The problem is that ‘prec’ specifies the number of significant digits to retain, and not the number of digits following the decimal point. Thus, if we try

from decimal import *
getcontext().prec = 2
sum = Decimal(2.5) + Decimal(10.47)
sum

we get the result Decimal('13') and not 12.97, as we might hope. We can solve this in two ways. First, we can enclose the quantities 2.5 and 10.47 in quotes to convert them to strings, and reset the prec to some large number. That is, we have

from decimal import *
getcontext().prec = 28
sum = Decimal('2.5') + Decimal('10.47')
sum

This gives the desired result of Decimal('12.97').

Second, if we don’t want to write the arguments to Decimal as strings, we can use the quantize() function, as follows:

from decimal import *
sum = Decimal(2.5) + Decimal(10.47)
pence = Decimal('0.01')
sum.quantize(pence)

This also gives the correct result of Decimal('12.97').

Despite their peculiarities, Decimals can be combined with other numeric types, although some caution is needed. You can perform the usual arithmetic operations combining Decimals and integers. For example, try the following (we’re assuming the prec is 28, the default):

from decimal import *
sum = Decimal('2.5') + Decimal('10.47')
sum * 12
sum ** 3
2 ** sum

See answers


sum * 12 -> Decimal(‘155.64’)
sum ** 3 -> Decimal(‘2181.825073’)
2 ** sum -> Decimal(‘8023.411077832104927916181357’)

Note that combining a Decimal with an integer always yields a Decimal result.

However, if you try combining a Decimal with a float, things go wrong. Try sum + 12.834, and you’ll get the error message “TypeError: unsupported operand type(s) for +: ‘decimal.Decimal’ and ‘float'”.  The error message means that the + operator is not defined when one of its operands is a Decimal and the other is a float.

To get around this, we can convert the Decimal to a float: float(sum) + 12.834.  As we’re now combining two floats, the answer is also a float: 25.804000000000002. As you can see, we’re back in the roundoff error problem again. If we want to avoid this, we can convert the float to a string and then to a Decimal, so we have sum + Decimal('12.834'), which now yields an exact Decimal result of Decimal('25.804'). If we try to convert the float to a Decimal without first enclosing it in quotes, we get roundoff error again. The statement sum3 + Decimal(12.834) gives us (assuming prec is set to its default value of 28): Decimal('25.80399999999999963051777740').