- Define a boolean type and enumerate its possible values
- Use relational operators to compare values
- Explain and use AND, OR and NOT operators
- Generate the truth table from simple boolean expression and implement a truth table as a boolean expression
- Explain when conditional statements are used
- Describe the evaluation order of if-elif-else statements in Python
- Use conditional statements in a Python program

Last time we introduced Boolean types, specifically the `bool`

type in Python.

```
s = "hello"
>>> s.startswith('h')
True
>>> s.startswith('b')
False
```

`bool`

s have only two values: `True`

and `False`

. As their name suggests they
are used to express what is true and what isn’t.

All Boolean operations can be implemented with a combination of the basic
boolean operations AND, OR and NOT. Or in Python `and`

, `or`

, and `not`

. The
first two are binary operations, i.e. have two inputs, the last is unary, i.e. has a
single input.

```
>>> not True
False
>>> not False
True
>>> True and False
False
>>> True or False
True
```

Boolean operators are often expressed as truth tables, e.g. `a <operator> b`

:

a | b | OR | AND |
---|---|---|---|

True | True | True | True |

True | False | True | False |

False | True | True | False |

False | False | False | False |

Much like arithmetic operators, boolean operators have precedence: `not`

,
then `and`

, then `or`

.

*PI Questions (Boolean operators)1*

We earlier saw some string methods that return `bool`

. Another common way to
generate `bool`

s is with relational operators applied to numerical (and other)
inputs, e.g.

`<, >, <=, >=, ==, !=`

Recall that in Python (unlike math), `=`

is assignment, and `==`

is equality
(with `!=`

implementing “not equals”).

```
>>> 1 < 2
True
>>> 2 > 1
True
>>> 2 != 1
True
>>> 2 == 2
True
>>> 2 == 2.0
True
>>> x = 2
>>> 1 < x < 3
True
>>> s == "hello"
True
>>> s[1] == "h"
False
>>> s[0] == "h"
True
>>> s[:2] == "he"
True
```

*PI Questions (relational operators)1*

One of the key uses for `bool`

s is making decisions. So far none of our
programs have made any choices. Being able to do so is very powerful.

The general pattern for *conditional statements* in Python:

```
if (boolean expression A):
statement1
statement2
elif (boolean expression B):
statement3
statement4
else:
statement5
statement6
statement7
statement8
```

If A evaluates to `True`

which statements will be executed? Statements 1,2,7,8.
What if A evaluates to `False`

? If B evaluates to `True`

, statements 3,4,7,8.
If neither A or B evaluate to `True`

, then statements 5,6,7,8 will execute.

Only one of the `if`

, `elif`

or `else`

is “selected” and its body executed,
even if multiple of the boolean expressions would have evaluated to true, and
that selection occurs in order.

Note `elif`

and `else`

are optional and no branch of the conditional needs to
be selected. Multiple `elif`

s are permitted.

Some other examples:

```
def is_odd(n):
if n % 2 == 1:
return True
else:
return False
def positivity(n):
if n == 0:
print(n, "is zero")
elif n > 0:
print(n, "is positive")
else:
print(n, "is negative")
```

*PI Questions (conditional statements)1*

A note about coding “best practices”. Can we write `is_odd`

more concisely?
Yes. Almost anytime we are returning a boolean from a conditional statement we can
do so more concisely, e.g.

```
def is_odd(n):
return (n % 2) == 1
```

Why is this better style? Recall that good coding style is often about minimizing the cognitive burden of reading code. When returning boolean values from within in if-else statement we first need understand the condition expression and then map it to separate, possibly different return values. That is additional cognitive burden compared to just needing to understand the condition expression.

We can calculate π via simulation. Consider a quarter circle inscribed inside a square of side 1 (i.e. with an area of 1). If we randomly select points inside the square, approximately a π/4 fraction of those points should be inside the quarter circle. By calculating the ratio of randomly sampled points inside the circle to the total number of sample, we can estimate the value of π. This approach is called Monte Carlo sampling.

Let’s implement a function `calculate_pi`

with a single parameter, the number of sampled points. What should we expect as we increase the number of samples? We will get closer and closer (probabilistically) to the actual value of pi. For example:

```
>>> calculate_pi(10)
3.2
>>> calculate_pi(100)
3.08
>>> calculate_pi(1000)
3.088
>>> calculate_pi(10000)
3.1604
>>> calculate_pi(100000)
3.14324
>>> calculate_pi(1000000)
3.141572
>>> calculate_pi(10000000)
3.1411364
```

Show a potential implementation…

```
import math, random
def calculate_pi(num_samples):
"""
Approximate pi via Monte Carlo sampling
Args:
num_samples: number of Monte Carlo samples
Returns:
Approximate value of pi
"""
in_circle = 0
for i in range(num_samples):
# Generate random "dart" inside unit square
x = random.uniform(0, 1)
y = random.uniform(0, 1)
# Determine distance to origin
dist = math.sqrt(x*x + y*y)
# Count number of darts inside the circle
if dist <= 1:
in_circle += 1 # equivalent to in_circle = in_circle + 1
# Calculate pi based on ratio of "darts" inside circle vs total samples
return (4 * in_circle) / num_samples
```