2. Computing with Python: an Introduction#

Welcome to programming!

What is the difference between Python and a calculator? We begin this first lesson by showing how Python can be used as a calculator, and we move into one of the most important programming structures – the loop. Loops allow computers to carry out repetetive computations, with just a few commands.

2.1. Python as a calculator#

Different kinds of data are stored as different types in Python. For example, if you wish to work with integers, your data is typically stored as an int. A real number might be stored as a float. There are types for booleans (True/False data), strings (like “Hello World!”), and many more we will see.

A more complete reference for Python’s numerical types and arithmetic operations can be found in the official Python documentation. The official Python tutorial is also a great place to start.

Python allows you to perform arithmetic operations: addition, subtraction, multiplication, and division, on numerical types. The operation symbols are +, -, *, and /.

Note in the code below, there are little “comments”. To place a short comment on a line of code, just put a hashtag # at the end of the line of code, followed by your comment.

2 + 3 # Addition
5
2 * 3 # Multiplication
6
5 - 11 # Subtracting integers
-6
5.0 - 11 # Subtracting an integer from a decimal (floating point number, or float)
-6.0
5 / 11 # Floating point division
0.45454545454545453
6 / 3
2.0
6 // 3 # Integer division
2
5 // 11
0
-12 // 5  
-3

The results are probably not too surprising, though the last two require a bit of explanation. Python interprets the input number 5 as an int (integer) and 5.0 as a float. “Float” stands for “floating point number,” which is a sort of decimal approximation to a real number. The word “float” refers to the fact that the decimal (or binary, for computers) point can float around (as in 1.2345 or 12.345 or 123.45 or 1234.5 or 0.00012345). There are deep computational issues related to how computers handle decimal approximations, and you can read about the IEEE standards if you’re interested.

Python enables different kinds of division. The single-slash division in Python 3.x gives a floating point approximation of the quotient. That’s why 5 / 11 and 6 / 3 both output floats. On the other hand, 5 // 11 and 6 // 3 yield integer outputs (rounding down) – this is useful, but one has to be careful!

In fact the designers of Python changed their mind. This tutorial assumes that you are using Python 3.x. If you are using Python 2.x, the command 5 / 11 would output zero.

Why use integer division // and why use floating point division? In practice, integer division is typically a faster operation. So if you only need the rounded result (and that will often be the case), use integer division. It will run much faster than carrying out floating point division then manually rounding down.

Observe that floating point operations involve approximation. The result of 5.0/11.0 might not be what you expect in the last digit. Over time, especially with repeated operations, floating point approximation errors can add up!

5.0 / 11.0
0.45454545454545453

Python allows you to group expressions with parentheses, and follows the order of operations that you learn in school.

(3 + 4) * 5
35
3 + (4 * 5)
23
3 + 4 * 5  
23

For number theory, division with remainder is an operation of central importance. Integer division provides the quotient, and the operation % provides the remainder. It’s a bit strange that the percent symbol is used for the remainder, but this dates at least to the early 1970s and has become standard across computer languages.

23 // 5  # Integer division
4
23 % 5  # The remainder after division
3

Python gives a single command for division with remainder. Its output is a tuple.

divmod(23,5)
(4, 3)
type(divmod(23,5))
tuple

All data in Python has a type, but a common complaint about Python is that types are a bit concealed “under the hood”. But they are not far under the hood! Anyone can find out the type of some data with a single command.

type(3)
int
type(3.0)
float
type('Hello')
str
type([1,2,3])
list

The key to careful computation in Python is always being aware of the type of your data, and knowing how Python operates differently on data of different types.

3 + 3
6
3.0 + 3.0
6.0
'Hello' + 'World!'
'HelloWorld!'
[1,2,3] + [4,5,6]
[1, 2, 3, 4, 5, 6]
3 + 3.0
6.0
3 + 'Hello!'  # Uh oh!
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[27], line 1
----> 1 3 + 'Hello!'  # Uh oh!

TypeError: unsupported operand type(s) for +: 'int' and 'str'

As you can see, addition (the + operator) is interpreted differently in the contexts of numbers, strings, and lists. The designers of Python allowed us to add numbers of different types: if you try to operate on an int and a float, the int will typically be coerced into a float in order to perform the operation. But the designers of Python did not give meaning to the addition of a number with a string, for example. That’s why we received a TypeError after trying to add a number to a string.

On the other hand, Python does interpret multiplication of a natural number with a string or a list.

3 * 'Hello!'
'Hello!Hello!Hello!'
0 * 'Hello!'
''
2 * [1,2,3]
[1, 2, 3, 1, 2, 3]

TRY IT! Create a string with 100 A’s (like AAA...).

Exponents in Python are given by the ** operator. The following lines compute 2 to the 1000th power, in two different ways.

2**1000
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
2.0**1000
1.0715086071862673e+301

As before, Python interprets an operation (**) differently in different contexts. When given integer input, Python evaluates 2**1000 exactly. The result is a large integer. A nice fact about Python, for mathematicians, is that it handles exact integers of arbitrary length! Many other programming languages (like C++) will give an error message if integers get too large in the midst of a computation.

New in version 3.x, Python implements long integers without giving signals to the programmer or changing types. In Python 2.x, there were two types: int for somewhat small integers (e.g., up to \(2^{31}\)) and long type for all larger integers. Python 2.x would signal which type of integer was being used, by placing the letter “L” at the end of a long integer. Now, in Python 3.x, the programmer doesn’t really see the difference. There is only the int type. But Python still optimizes computations, using hardware functionality for arithmetic of small integers and custom routines for large integers. The programmer doesn’t have to worry about it most of the time.

For scientific applications, one often wants to keep track of only a certain number of significant digits (sig figs). If one computes the floating point exponent 2.0**1000, the result is a decimal approximation. It is still a float. The expression “e+301” stands for “multiplied by 10 to the 301st power”, i.e., Python uses scientific notation for large floats.

type(2**1000)
int
type(2.0**1000)
float

2.2. Calculating with booleans#

A boolean (type bool) is the smallest possible piece of data. While an int can be any integer, positive or negative, a boolean can only be one of two things: True or False. In this way, booleans are useful for storing the answers to yes/no questions.

Questions about (in)equality of numbers are answered in Python by operations with numerical input and boolean output. Here are some examples. A more complete reference is in the official Python documentation.

3 > 2
True
type(3 > 2)
bool
10 < 3
False
2.4 < 2.4000001
True
32 >= 32
True
32 >= 31
True
2 + 2 == 4
True

TRY IT! Which number is bigger: \(23^{32}\) or \(32^{23}\)?

The expressions <, >, <=, >= are interpreted here as operations with numerical input and boolean output. The symbol == (two equal symbols!) gives a True result if the numbers are equal, and False if the numbers are not equal. An extremely common typo is to confuse = with ==. But the single equality symbol = has an entirely different meaning, as we shall see.

Using the remainder operator % and equality, we obtain a divisibility test.

63 % 7 == 0  # Is 63 divisible by 7?
True
101 % 2 == 0  # Is 101 even?
False

TRY IT! Determine whether 1234567890 is divisible by 3.

Booleans can be operated on by the standard logical operations: and, or, not. In ordinary English usage, “and” and “or” are conjunctions, while here in Boolean algebra, “and” and “or” are operations with Boolean inputs and Boolean output. The precise meanings of “and” and “or” are given by the following truth tables.

and

True

False

True

True

False

False

False

False


or

True

False

True

True

True

False

True

False

True and False
False
True or False
True
True or True
True
not True
False
(2 > 3) and (3 > 2)
False
(1 + 1 == 2) or (1 + 1 == 3)
True
not (-1 + 1 >= 0)
False
2 + 2 == 4
True
2 + 2 != 4  # For "not equal", Python uses the operation `!=`.
False
2 + 2 != 5  # Is 2+2 *not* equal to 5?
True
not (2 + 2 == 5)  # The same as above, but a bit longer to write.
True

TRY IT! Experiment to see how Python handles a double or triple negative, i.e., something with a not not.

Python does give an interpretation to arithmetic operations with booleans and numbers.

False * 100
0
True + 13
14

This ability of Python to interpret operations based on context is a mixed blessing. On one hand, it leads to handy shortcuts – quick ways of writing complicated programs. On the other hand, it can lead to code that is harder to read, especially for a Python novice. Good programmers aim for code that is easy to read, not just short!

The Zen of Python is a series of 20 aphorisms for Python programmers. We can see it by running the command import this.

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

2.3. Declaring variables#

A central feature of programming is the declaration of variables. When you declare a variable, you are storing data in the computer’s memory and you are assigning a name to that data. Both storage and name-assignment are carried out with the single equality symbol =.

e = 2.71828

With this command, the float 2.71828 is stored somewhere inside your computer, and Python can access this stored number by the name “e” thereafter. So if you want to compute “e squared”, a single command will do.

e * e
7.3890461584
type(e)
float

You can use just about any name you want for a variable, but your name must start with a letter, must not contain spaces, and your name must not be an existing Python word. Characters in a variable name can include letters (uppercase and lowercase) and numbers and underscores _.

So e is a valid name for a variable, but type is a bad name. It is very tempting for beginners to use very short abbreviation-style names for variables (like dx or vbn). But resist that temptation and use more descriptive names for variables, like difference_x or very_big_number. This will make your code readable by you and others!

There are different style conventions for variable names. We use lowercase names, with underscores separating words, roughly following Google’s style conventions for Python code.

my_number = 17
my_number < 23
True

After you declare a variable, its value remains the same until it is changed. You can change the value of a variable with a simple assignment. After the above lines, the value of my_number is 17.

my_number = 3.14

This command reassigns the value of my_number to 3.14. Note that it changes the type too! It effectively overrides the previous value and replaces it with the new value.

Often it is useful to change the value of a variable incrementally or recursively. Python, like many programming languages, allows one to assign variables in a self-referential way.

S = 0
S = S + 1
S = S + 2
S = S + 3
print(S)
6

The first line S = 0 is the initial declaration: the value 0 is stored in memory, and the name S is assigned to this value.

The next line S = S + 1 looks like nonsense, as an algebraic sentence. But reading = as assignment rather than equality, you should read the line S = S + 1 as assigning the value S + 1 to the name S. When Python interprets S = S + 1, it carries out the following steps.

  1. Compute the value of the right side, S+1. (The value is 1, since S was assigned the value 0 in the previous line.)

  2. Assign the name S to this new value. (Now S has the value 1.)

Well, this is a slight lie. Python probably does something more efficient, when given the command S = S + 1, since such operations are hard-wired in the computer and the Python interpreter is smart enough to take the most efficient route. But at this level, it is most useful to think of a self-referential assignment of the form X = expression(X) as a two step process as above.

  1. Compute the value of expression(X).

  2. Assign this value to the name X.

Now consider the following three commands.

my_number = 17
new_number = my_number + 1
my_number = 3.14

What are the values of the variables my_number and new_number, after the execution of these three lines?

To access these values, you can use the print function.

print(my_number)
print(new_number)
3.14
18

Python is an interpreted language, which carries out commands line-by-line from top to bottom. So consider the three lines

my_number = 17
new_number = my_number + 1
my_number = 3.14

Line 1 sets the value of my_number to 17. Line 2 sets the value of new_number to 18. Line 3 sets the value of my_number to 3.14. But Line 3 does not change the value of new_number at all.

(This will become confusing and complicated later, as we study mutable and immutable types.)

2.4. Lists and ranges#

Python stands out for the central role played by lists. A list is what it sounds like – a list of data. Data within a list can be of any type. Multiple types are possible within the same list! The basic syntax for a list is to use brackets to enclose the list items and commas to separate the list items.

type([1,2,3])
list
type(['Hello',17])
list

There is another type called a tuple that we will use less often. Tuples use parentheses for enclosure instead of brackets.

type((1,2,3))
tuple

There’s another list-like type in Python 3, called the range type. Ranges are kind of like lists, but instead of plunking every item into a slot of memory, ranges just have to remember three integers: their start, their stop, and their step.

The range command creates a range with a given start, stop, and step. If you only input one number, the range will start at zero and use steps of one and will stop just before the given stop-number.

One can create a list from a range (plunking every term in the range into a slot of memory), by using the list command. Here are a few examples.

type(range(10)) # Ranges are their own type, in Python 3.x.  Not in Python 2.x!
range
list(range(10)) # Let's see what's in the range.  Note it starts at zero!  Where does it stop?
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

A more complicated two-input form of the range command produces a range of integers starting at a given number, and terminating before another given number.

list(range(3,10))
[3, 4, 5, 6, 7, 8, 9]
list(range(-4,5))
[-4, -3, -2, -1, 0, 1, 2, 3, 4]

This is a common source of difficulty for Python beginners. While the first parameter (-4) is the starting point of the list, the list ends just before the second parameter (5). This takes some getting used to, but experienced Python programmers grow to like this convention.

The length of a list can be accessed by the len command.

len([2,4,6])
3
len(range(10))  # The len command can deal with lists and ranges.  No need to convert.
10
len(range(10,100)) # Can you figure out the length, before evaluating?
90

The final variant of the range command (for now) is the three-parameter command of the form range(a,b,s). This produces a list like range(a,b), but with a “step size” of s. In other words, it produces a list of integers, beginning at a, increasing by s from one entry to the next, and going up to (but not including) b. It is best to experiment a bit to get the feel for it!

list(range(1,10,2))
[1, 3, 5, 7, 9]
list(range(11,30,2))
[11, 13, 15, 17, 19, 21, 23, 25, 27, 29]
list(range(-4,5,3))
[-4, -1, 2]
list(range(10,100,17))
[10, 27, 44, 61, 78, 95]

This can be used for descending ranges too, and observe that the final number b in range(a,b,s) is not included.

list(range(10,0,-1))
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

How many multiples of 7 are between 10 and 100? We can find out pretty quickly with the range command and the len command (to count).

list(range(10,100,7))  # What list will this create?  It won't answer the question...
[10, 17, 24, 31, 38, 45, 52, 59, 66, 73, 80, 87, 94]
list(range(14,100,7))  # Starting at 14 gives the multiples of 7.
[14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84, 91, 98]
len(range(14,100,7))  # Gives the length of the list, and answers the question!
13

2.5. Iterating over a range#

Computers are excellent at repetitive reliable tasks. If we wish to perform a similar computation, many times over, a computer a great tool. Here we look at a common and simple way to carry out a repetetive computation: the “for loop”. The “for loop” iterates through items in a list or range, carrying out some action for each item. Two examples will illustrate.

for n in [1,2,3,4,5]:
    print(n*n)
1
4
9
16
25
for s in ['I','Am','Python']:
    print(s + "!")
I!
Am!
Python!

The first loop, unraveled, carries out the following sequence of commands.

n = 1
print(n*n)
n = 2
print(n*n)
n = 3
print(n*n)
n = 4
print(n*n)
n = 5
print(n*n)
1
4
9
16
25

But the “for loop” is more efficient and more readable to programmers. Indeed, it saves the repetition of writing the same command print(n*n) over and over again. It also makes transparent, from the beginning, the range of values that n is assigned to.

When you read and write “for loops”, you should consider how they look unravelled – that is how Python will carry out the loop. And when you find yourself faced with a repetetive task, you might consider whether it may be wrapped up in a for loop. Most of the time, you should try to write code that never repeats the same task. If there is repetition, wrap it in a loop.

Try to unravel the loop below, and predict the result.

P = 1
for n in range(1,6):
    P = P * n
print(P)
120

This might have been difficult! So what if you want to trace through the loop, as it goes? Sometimes, especially when debugging, it’s useful to inspect every step of the loop to see what Python is doing. We can inspect the loop above, by inserting a print command within the scope of the loop.

P = 1
for n in range(1, 6):
    P = P * n
    print("n is", n, "and P is", P)
print(P)
n is 1 and P is 1
n is 2 and P is 2
n is 3 and P is 6
n is 4 and P is 24
n is 5 and P is 120
120

Here we have used the print command with strings and numbers together. In Python 3.x, you can print multiple things on the same line by separating them by commas. The “things” can be strings (enclosed by single or double-quotes) and numbers (int, float, etc.).

print("My favorite number is", 17)
My favorite number is 17

Let’s analyze the loop syntax in more detail.

P = 1
for n in range(1,6):
    P = P * n  # this command is in the scope of the loop.
    print("n is", n, "and P is", P)  # this command is in the scope of the loop too!
print(P)

The “for” command ends with a colon :, and the next two lines are indented. The colon and indentation are indicators of scope. The scope of the for loop begins after the colon, and includes all indented lines. The scope of the for loop is what is repeated in every step of the loop (in addition to the reassignment of n).

If we change the indentation, it changes the scope of the for loop.

P = 1
for n in range(1,6):
    P = P * n
print("n is",n,"and P is",P)
print(P)
n is 5 and P is 120
120

Scopes can be nested by nesting indentation.

for x in [1,2,3]:
    for y in ['a', 'b']:
        print(x,y)
1 a
1 b
2 a
2 b
3 a
3 b

TRY IT! Create a nested loop which prints 1 a then 2 a then 3 a then 1 b then 2 b then 3 b.

Among popular programming languages, Python is particular about indentation. Other languages indicate scope with open/close braces, for example, and indentation is just a matter of style. By requiring indentation to indicate scope, Python effectively removes the need for open/close braces, and enforces a readable style.

We have now encountered data types, operations, variables, and loops. Taken together, these are powerful tools for computation! Now complete the following exercises for more practice.

2.6. Exercises#

  1. What data types have you seen, and what kinds of data are they used for? Can you remember them without looking back?

  2. How is division / interpreted differently for different types of data?

  3. How is multiplication * interpreted differently for different types of data?

  4. What is the difference between 100 and 100.0, for Python?

  5. Did you look at the truth tables closely? Can you remember, from memory, what True or False equals, or what True and False equals?

  6. How might you easily remember the truth tables? How do they resemble the standard English usage of the words “and” and “or”?

  7. If you wanted to know whether a number, like 2349872348723, is a multiple of 7 but not a multiple of 11, how might you write this in one line of Python code?

  8. You can chain together and commands, e.g., with an expression like True and True and True (which would evaluate to True). You can also group booleans, e.g., with True and (True or False). Experiment to figure out the order of operations (and, or, not) for booleans.

  9. The operation xor means “exclusive or”. Its truth table is: True xor True = False and False xor False = False and True xor False = True and False xor True = True. How might you implement xor in terms of the usual and, or, and not?

  10. What is the difference between = and == in the Python language?

  11. If the variable x has value 3, and you then evaluate the Python command x = x * x, what will be the value of x after evaluation?

  12. Imagine you have two variables a and b, and you want to switch their values. How could you do this in Python?

  13. Kepler’s third law states that for a planet in circular orbit around an object of mass \(M\), one has \(4 \pi^2 r^3 = G M t^2\). We can use this to estimate the mass of the sun, from other astronomically observable quantities. Look up \(G\) (the gravitational constant, estimated by experiment on Earth) and \(r\) (the distance from the Earth to the sun, in meters). Compute \(t\), the number of seconds it takes for the Earth to go around the sun (365.25 days). Finally use all of this to estimate \(M\), the mass of the sun. Your solution should use 5-10 lines of Python code.

  14. If a and b are integers, what is the length of range(a,b)? Express your answer as a formula involving a and b.

  15. Use a list and range command to produce the list [1,2,3,4,5,6,7,8,9,10].

  16. Create the list [1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5] with a single list and range command and another operation.

  17. How many multiples of 3 are there between 300 and 3000 (including 300 and 3000)?

  18. Describe how Python interprets division with remainder when the divisor and/or dividend is negative.

  19. What is the remainder when \(2^{90}\) is divided by \(91\)?

  20. How many multiples of 13 are there between 1 and 1000?

  21. How many odd multiples of 13 are there between 1 and 1000?

  22. What is the sum of the numbers from 1 to 1000?

  23. What is the sum of the squares, from \(1 \cdot 1\) to \(1000 \cdot 1000\)?

  24. Euler proved that

\[\frac{1}{1^4} + \frac{1}{2^4} + \frac{1}{3^4} + \cdots = \frac{\pi^4}{C},\]

for some positive integer \(C\). Use Python to guess what \(C\) is.

2.7. Explorations#

Now that you have learned the basics of computation in Python and loops, we can start exploring some interesting mathematics! We are going to look at approximation here – some ancient questions made easier with programming.

2.7.1. Exploration 1: Approximating square roots.#

We have seen how Python can do basic arithmetic – addition, subtraction, multiplication, and division. But what about other functions, like the square root? In fact, Python offers a few functions for the square root, but that’s not the point. How can we compute the square root using only basic arithmetic?

Why might we care?

  1. We might want to know the square root of a number with more precision than the Python function offers.

  2. We might want to understand how the square root is computed… under the hood.

  3. Understanding approximations of square roots and other functions is important, because we might want to approximate other functions in the future (that aren’t pre-programmed for us).

Here is a method for approximating the square root of a number \(X\).

  1. Begin with a guess \(g\).

  2. Observe that \(g \cdot (X / g) = X\). Therefore, among the two numbers \(g\) and \((X/g)\), one will be less than or equal to the square root of X, and the other will be greater than or equal to the square root.

  3. Take the average of \(g\) and \((X/g)\). This will be closer to the square root than \(g\) or \(X/g\) (unless your guess is exactly right!)

  4. Use this average as a new guess… and go back to the beginning.

Now implement this in Python to approximate the square root of 2. Use a loop, so that you can go through the approximation process 10 times or 100 times or however many you wish. Explore the effect of different starting guesses. Would a change in the averaging function improve the approximation? How quickly does this converge? How does this change if you try square roots of different positive numbers?

Write your code (Python) and findings (in Markdown cells) in a readable form. Answer the questions in complete sentences.

2.7.2. Exploration 2: Approximating e and pi.#

Now we approximate two of the most important constants in mathematics: e and pi. There are multiple approaches, but e is pretty easy with the series expansion of e^x. First, approximate e by evaluating the Taylor series expansion of e^x at x=1. I.e., remember from calculus that $\(e^x = 1 + x + \frac{1}{2} x^2 + \frac{1}{3!} x^3 + \frac{1}{4!} x^4 + \cdots.\)$ How many terms are necessary before the float stabilizes? Use a loop, with a running product for the factorials and running sums for the series.

Next we will approximate pi, which is much more interesting. We can try a few approaches. For a series-approach (like e), we need a series that converges to pi. A simple example is the arctangent atan(x). Recall (precalculus!) that \(atan(1) = \pi/4\). Moreover, the derivative of \(atan(x)\) is \(1 / (1+x^2)\).

  1. Figure out the Taylor series of \(1 / (1+x^2)\) near \(x=0\). Note that this is a geometric series! So you can use the formula \(1 / (1-r) = 1 + r + r^2 + r^3 + \cdots\).

  2. Figure out the Taylor series of \(atan(x)\) near \(x=0\) by taking the antiderivative, term by term, of the above.

  3. Try to estimate \(\pi\) with this series, using many terms of the series.

Now we’ll accelerate things a bit. There’s a famous formula of Machin (1706) who computed the first hundred digits of \(\pi\). We’ll use his identity:

\(\pi/4 = 4 \cdot atan(1/5) - atan(1 / 239)\).

This isn’t obvious, but there’s a tedious proof using sum/difference identities in trig.

Try using this formula now to approximate \(\pi\), using your Taylor series for \(atan(x)\). It should require fewer terms.

Now let’s compare this to Archimedes’ method. Archimedes approximated pi by looking at the perimeters \(p(n)\) and \(P(n)\) of (\(2^n\))-gons inscribed in and circumscribed around a unit circle. So \(p(2)\) is the perimeter of a square inscribed in the unit circle. \(P(2)\) is the perimeter of a square circumscribed around a unit circle.

Archimedes proved the following (not in the formulaic language of algebra): For all \(n \geq 2\),

(P-formula) \(P(n+1) = \frac{2 p(n) P(n)}{p(n) + P(n)}\).

(p-formula) \(p(n+1) = \sqrt{ p(n) P(n+1) }\).

  1. Compute \(p(2)\) and \(P(2)\).

  2. Use these formulas to compute \(p(10)\) and \(P(10)\). Use this to get a good approximation for \(\pi\)!

We could use our previous sqrt function. But here is a fancier high-precision approach. “mpmath” is a Python package for high-precision calculation. It should be accessible through Google Colab. You can read the full documentation at http://mpmath.org/doc/current/

First we load the package and print its status.

from mpmath import *
print(mp)
Mpmath settings:
  mp.prec = 53                [default: 53]
  mp.dps = 15                 [default: 15]
  mp.trap_complex = False     [default: False]

The number mp.dps is (roughly) the number of decimal digits that mpmath will keep track of in its computations. mp.prec is the binary precision, a bit more than 3 times the decimal precision. We can change this to whatever we want.

mp.dps = 50 # Let's try 50 digits precision to start.
print(mp)
Mpmath settings:
  mp.prec = 169               [default: 53]
  mp.dps = 50                 [default: 15]
  mp.trap_complex = False     [default: False]

mpmath has a nice function for square roots. Compare this to your approximation from before!

sqrt(2)  # mpf(...) stands for an mp-float.
mpf('1.4142135623730950488016887242096980785696718753769468')
type(sqrt(2)) # mpmath stores numbers in its own types!
mpmath.ctx_mp_python.mpf
4*mp.atan(1) # mpmath has the arctan built in.  This should be pretty close to pi!
mpf('3.1415926535897932384626433832795028841971693993751068')

Now try Archimedes’ approximation of pi. Use the mpmath sqrt function along the way. How many iterations do you need to get pi correct to 100 digits? Compare this to the arctan-series (not the mpmath atan function) via Machin’s formula.