> For the complete documentation index, see [llms.txt](https://curropb.gitbook.io/python-notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://curropb.gitbook.io/python-notes/python-lesson-3.md).

# Python Lesson 3

## Lesson outline

1. Python scalars
2. Native Python lists
3. Python control structures: conditionals
4. Python control structures: loops
5. Application to NumPy data
6. Exercises

## Standard Python scalar types

Python has several built-in types for handling numerical data, strings, Boolean (logical `True` or `False`) values, and dates and times; they are called scalars.

### Numeric scalars

There are two numeric scalars, *float* and *int*. The *float* type corresponds to a double-precision (64-bit) floating-point number. The *int* type is an arbitrary precision signed integer.

```
integer_example = 496
divisor_sum = 1+2+4+8+16+31+62+124+248
print(496//31)
print(divisor_sum)
```

Floats are double precision by default (no single precision type) and they can be expressed in scientific notation

```
float_example = 0.5*(1.0 + np.sqrt(5.0))
print(float_example)
```

### Strings

Python is considered a very powerful programming language for string manipulation. Strings are defined surrounding the text by single or double quotes

```
string_1 = 'This is a string'
string_2 = "This is another string"
string_3 = "Let's roll!"
string_4 = "She said \"Let's roll!\""
```

You can use one or the other, but always start and end with the same. Depending the one you are using, you can include the other in the string, as in `string_3`. If you need to include the same quote than the marker in the string you have to escape it with a backslash, as in `string_4`.

Usin triple quotes you can define a multiline string

```
string_3 = '''This is a multiline string...

The string follows here...
'''
```

In strings you can use the `count` method

```
print(string_1.count("i"))
print(string_3.count("\n")) # Newline characters
```

As in the previous example, the backslash is an *escape character* used to specify special characters.

You can access string characters by its index

```
print(string_1[2])
print(string_1[2:])
```

Strings are *immutable* objects, you cannot alter them by index, try

```
string_1[2] = "f"
```

However, you can append or prepend characters to a string with the + operator, that concatenates them

```
string_4 = "Betis"
print("Viva el " + string_4 + "!")
```

You can also use the *replace* method

```
string_5 = string_1.replace("a", "another")
print(string_5)
```

You can join two strings using the sum symbol. This is very used when making graphs. To add a numerical or other scalar to the string you can use the `str` function.

```
Str_legend_0 = "This may be the figure for par = "
par_int = 12
Str_legend_int = Str_legend_0 + str(par_int) + " units"
par_float = 2.2221
Str_legend_float = Str_legend_0 + str(par_float) + " units"
print(Str_legend_int)
print(Str_legend_float)
```

You can further control strings using the `format()` method as in the following examples

```
example_0 = "The approximate value of the golden section is " + str(0.5*(1.0 + np.sqrt(5.0)))
print(example_0)
example_1 = "The approximate value of {0} is {1:.5f}".format("the golden section", 0.5*(1.0 + np.sqrt(5.0)))
print(example_1)
example_2 = "The approximate value of {0} is {1:.8g}".format("the golden section", 0.5*(1.0 + np.sqrt(5.0)))
print(example_2)
summing = "{1:d} plus {2:d} is equal to {0:d}"
print(summing.format(3,4,-1))
```

For more info check <https://www.python.org/dev/peps/pep-3101/#format-specifiers>.

### Booleans

The Booleans values in Python are `True` and `False` and can be combined with the keywords `or`, `and`, and `not`.

```
print(True and False)
print(True and not False)
print(True or False)
```

### The `None` type

The Python null type is `None`, which is returned by any function that does not explicitly return a value.

```
var_1 = None
print(var_1 is None)
print(string_4 is not None)
```

When functions are defined, `None` is a common default value for function arguments (check Lesson 4).

### Type casting

You can transform among Python objects with the `str`, `float`, `int`, and `bool` commands

```
string_6 = str(float_example)
print(string_6 + string_1)
#
print(2*float(string_6))
#
print(2*int(float_example))
#
print(bool(string_6))
#
print(bool(0))
print(bool(0.0))
print(bool(''))
print(bool(' '))
```

## Native Python Lists

The native Python data structures are *lists*, *dicts* (AKA *hashes*), *tuples*, and *sets*. In this lesson we cover only lists and the rest will be covered in the following lesson.

### Defining Python Lists

A *list* is a sequence of Python objects (maybe scalars or not, and may have also different types) that has variable lenght and is mutable. They can be defined using square brackets or the `list` command.

```
list_example = ["This ", "is ", "a ", "list ", "of ", "string"]
print(list_example)
list_example[5] = "strings"
print(list_example)
```

You can slice Python lists

```
print(list_example[0:3])
print(list_example[-3:])
```

As it happens with Numpy, when dealing with native Python structures, one needs to be always aware that Python uses the so called *pass by reference* and not the *pass by value* strategy of other programming languages. Thus, an assignment implies a reference to data in the righthand side. If we execute

```
list_example_2 = [1,2,3,4.0,"Hi"]
list_example_3 = list_example_2
```

the data are not copied and we do not have two data instances. Instead, both lists, `list_example_2` and `list_example_3` point to the same data. Having this in mind, we can understand what follows

```
print(list_example_2)
print(list_example_3)
list_example_2[4] = "Hello"
print(list_example_2)
print(list_example_3)
```

You can use the `list` function if you want to make a copy of the list

```
list_example_2 = [1,2,3,4.0,"Hi"]
list_example_3 = list(list_example_2)
list_example_2[4] = "Hello"
print(list_example_2)
print(list_example_3)
```

We will revisit this important aspect once we arrive to the subject of function definition in Python, where we should be careful to avoid unwanted *side effects*.

You can also create nested lists

```
nested_list = [["wasabi", "sushi", "sashimi", "miso"],["taco", "gringa", "enchilada", "carnitas"]]
print(nested_list[0][2])
```

### Modifying Python lists

And you can add elements to the end of a list using the `append` method.

```
list_example.append(" Hello!")
print(list_example)
#
aa = [] # This is an empty list (Boolean evaluates to False)
print(aa)
aa.append(float_example)
print(aa)
```

The `insert` method (computationally more expensive than the `append` method) allows for the insertion of elements at any place in the list

```
list_example.insert(2, "Cheerio!")
print(list_example)
```

The opposite method to `insert` is `pop`, which removes an element at a given place of the list

```
list_example.pop(2)
print(list_example)
```

You can also remove elements by value using the `remove` method, which removes the first appearance of a given list element.

```
list_example.remove("list ")
print(list_example)
```

The `in` keyword allow to check if there exists an element of the list

```
print("Bye!" in list_example)
print(" Bye!" in list_example)
```

And you can obtain the index of a given element with the `index()` method that finds the given element and returns its position (remember, indeces start in zero). If the same element is present multiple times, the index of the first occurrence of the element is returned.

```
print(list_example.index("is "))
```

Adding two lists concatenates them

```
list_example_4 = list_example_2 + list_example_3
print(list_example_4)
```

You can use the method `extend` also to join two lists, which is more efficient than directly adding them

```
list_example_2.extend(list_example_3)
print(list_example_2)
```

When doing calculations, one should always have in mind that a native Python list is a sequence of objects that can be of different types. This flexibility comes upon a cost on efficiency when compared to Numpy ndarrays, that store data of a single type in contiguous memory blocks.

## Making choices in Python: *conditionals*

Conditional alter the flow of a program depending on statements `True` or `False` value.

Possible conditionals

1. `a == b` : `True` if `a` equals `b`
2. `a != b` : `True` if `a` is not equal to `b`
3. `a < b`, `a <= b` : `True` if `a` is less than (less than or equal) to `b`
4. `a > b`, `a >= b` : `True` if `a` is greater than (greater than or equal) to `b`
5. `a is b` :  `True` if `a` and `b` reference the same Python object
6. `a is not b` : `True` if `a` and `b` reference different Python objects

### The `if ... elif ... else` control structure

The simplest case is the single `if` conditional with syntax

```
if (conditional):
    # if block of code
```

The indented code block only is run if the `conditional` statement evaluates to `True`.

```
if (float_example > 0):
    print("Positive number")
```

You can test several alternatives with `elif` blocks and add a final default clause if none of the previous are true using `else`

```
if (float_example > 0):
    print("Positive number")
elif (float_example < 0):
    print("Positive number")
else:
    print("This is zero...")
```

We can use this conditional structure to calculate the body mass index from the height and weight of a person (in *m* and *kg* units)

```
weight = 80
height = 1.80
bmi_value = weight/height**2
#
if bmi_value < 15:
    bmi_range = "Very severely underweight"
elif bmi_value < 16:
    bmi_range = "Severely underweight"
elif bmi_value < 18.5:
    bmi_range = "Underweight"
elif bmi_value < 25:
    bmi_range = "Normal(healthy weight)"
elif bmi_value < 30:
    bmi_range = "Overweight"
elif bmi_value < 35:
    bmi_range = "Obese Class I (Moderately obese)"
elif bmi_value < 40:
    bmi_range = "Obese Class II (Severely obese)"
else:
    bmi_range = "Obese Class III (Very severely obese)"

## 
print(bmi_value, " ", bmi_range)
```

### The *ternary* expression

This expression combines an `if...else..` statement with *single blocks* formed by a *single command* into a single line. It's useful and terse but it does not help increasing the readability of the code.

The syntax is

```
statement_1 if (conditional) else statement_2
```

and it is equivalent to

```
if (conditional):
    statement_1  
else:
    statement_2
```

An example is

```
a = 1001
print("Variable larger than 1000") if (a > 1000) else print("Variable less than or equal to 1000")
```

## Control Structures in Python: *loops*

Control structures alter the program flow. We explain the `for` and `while` control structures.

### The `for` loop

The `for` control structure defines a loop over an *iterator* with a syntax

```
for value in collection:
    # do something
```

A string is a possible iterator

```
for string_char in string_1:
    print(string_char)
```

You can control the loop flow using the keywords `continue` and `break`. The `continue` statement skip the rest of the block and continues with the next iteration

```
for string_char in string_1:
    if (string_char == "i" or string_char == "a"):
    continue
    else:
    print(string_char)
```

The `break` statement finish the loop and continues with the ensuing program statements.

```
for string_char in string_1:
    if (string_char == "o" or string_char == "u"):
    break
    else:
    print(string_char)
```

The Python statement `range` is specially useful for working with loops. The command `range(start, stop, step)` provides and iterator starting in `start`, ending in `stop-1` and with differences of `step` (default value one).

In this way we can sum all multiples of 7 or 13 from zero to ten thousand

```
total = 0
for number in range(10000):
    if (number % 7 == 0 or number % 13 == 0): 
    total = total + number
    print(number, total)
print("Total is = ", total)
```

If we want to sum all even integers from 2 to 100 we proceed as follows

```
total = 0
for number in range(2,101,2):
    total += number
    print(number, total)
print("Total is = ", total)
```

Note that statements as `total = total + number` are so common that they can be expressed more succintly as `total += number`. There are equivalent statements `-=`, `*=` and `/=`.

The `list` statement allows to materialize an iterator

```
iterator = range(10)
print(iterator)
print(list(iterator))
```

A very common situation is that in a loop we want to access the indexes of the elements that are being iterated. This can be done as follows

```
index_val = 0
for element in iterator:
    # work with element and with index = index_val
    index_val += 1
```

As this is a common need, Python has a built-in sequence method called `enumerate` that returns a sequence of `index, values` (a tuple, explained in next lesson).

```
for index, element in enumerate(iterator):
    # work with element and with index = index_val
```

### The `while` loop

The `while` control structure defines a loop depending on a condition. The loop block is run until the conditional becomes `False` with a syntax

```
while (conditional):
    # do something
```

For example

```
total = 1
value = 1
while (total < 5000):
    value += 1
    total *= value
    print(value, total)

print("Value and total after loop are", value, total)
```

You can also use the keywords `continue` and `break` to control a `while` loop flow.

Sometimes you can fall into an infinite loop as this one

```
while (1):
    print("What a mess...")
```

In this case it is important to know how to interrupt the loop: click on menu entry `Kernel -> Interrupt`. You can find an application of an infinite loop in the next lesson.

Sometimes loops become infinite unexpectedly. The following example illustrates the point mentioned above about the possible problems arising when using conditionals with floats. The *while* loop, seemingly inoffensive, depending on the value of the `i2` variable can end up as an infinite loop (try *e.g.* `i2 = 0.3,0.7,0.8,0.9`).

```
i1, i2 = 0, 0.9
while i1 != i2:
    print(i1)
    i1 += 0.1
```

### Testing floats equality (\*)

Be always aware that due to the finite precision in their internal representation and rounding/numerical errors conditionals can be tricky when applied to floats. As a rule, testing the equality of two floats `x` and `y` as `x==y` is highly discouraged and may have unpredictable side effects.

The way to avoid such problems is to replace float equality comparisons for inequalities, in this case `while i1 <= i2:` or, even better, compare integer quantities. When you need to compare to floats, define a tolerance -depending on the nature of the problem- and compare the difference of the two values versus this tolerance. You can also check the useful NumPy function `np.isclose`.

## Application to NumPy data

We can apply the control structures to NumPy data. If we load again a set of data

```
metdata_orig = np.loadtxt(fname='files/TData/T_Alicante_EM.csv', delimiter=',', skiprows=1) 
temp_data = metdata_orig[:,1:]
#+END_S RC

We can check whether these data have a maximum or minimum above some threshold

#+BEGIN_SRC python :results output
max_value = 30
min_value = 10
if (temp_data.max() >= max_value):
    print("These data have a maximum temp larger or equal to ", max_value)
else:
    print("The max temperature limit is not broken.")
#
if (temp_data.min() <= min_value):
    print("These data have a minimum T less than or equal to ", min_value)
else:
    print("The min temperature limit is not broken.")
#
if (temp_data.max() >= max_value and temp_data.min() <= min_value):
    print("These data have a maximum temp larger or equal to ", max_value, " AND a minimum T less than or equal to ", min_value)
else:
    print("Both max and min temperature limits are not broken simultaneously")
```

If, for example, we want to check how many years have attained a monthly average temperature larger than a given threshold we can do this as follows

```
max_value = 30
nyears = 0
for index_year in range(0,136):
    if (temp_data[index_year,:].max() >= max_value):
    print(metdata_orig[index_year,0])
    nyears += 1
print("A total of {0} years have one or more average monthly temperatures larger than {1} degrees Celsius.".format( nyears, max_value))
```

We can also, using a second loop, print which months have temperatures larger than the stablished threshold.

```
max_value = 27
nyears = 0
for index_year in range(0,136):
    if (temp_data[index_year,:].max() >= max_value):
    for index_month in range(0,12):
        if (temp_data[index_year,index_month] >= max_value):
        print(metdata_orig[index_year,0], index_month)
    print("")
    nyears += 1
print("A total of ", nyears, " have at least an average monthly temperature larger than ", max_value, "degrees Celsius.")
```

Of course, you can skip loops using NumPy Boolean matrices indexing. Last commands can be replaced to a very concise form as

```
max_value = 29
min_value = 10
print(np.any(temp_data > max_value))
print(np.any(temp_data < min_value))
print(np.any(temp_data > max_value) and np.any(temp_data < min_value))
print(np.sum(np.any(temp_data >= max_value, axis = 1)))
print(np.sum(np.any(temp_data >= max_value, axis = 0))) # What means this number?
```

The `np.any` NumPy command tests whether any array element along a given axis evaluates to `True`. Note that Boolean values are treated as 1 and 0 in arithmetic operations.

To find elements in an array verifying a given condition it is of interest the function `np.where`. In can be used in two ways. The first one just specifying the condition provides the positions where the condition is fulfilled:

```
x=np.arange(9).reshape(3,3)
print(np.where(x>1))
```

This is equivalent to the use of:

```
print(np.asarray(x>1).nonzero())
```

which provides the position or the content

```
print(np.asarray(x>1))
```

The other option is to specify the condition and to provide two matrices that should be broadcastable. This use resembles the ternary expression

```
np.where(x>1,x,-1)
```

if true you obtain the corresponding element of `x` and `-1` otherwise.

In this example we use two matrices

```
np.where(x>5,[["H","H","H"],["H","H","H"],["H","H","H"]],[["s","s","s"],["s","s","s"],["s","s","H"]])
```

## Exercises

* **Exercise 3.1:** Meking use of conditionals compute the equivalence between the age of a dog and a human taking into account the rules that follow: (1) A one-year-old dog is equivalent to a 14-year-old human being. (2) A dog that is two years old is considered equivalent to a 22 year old person. (3) Each additional dog year is equivalent to five human years.
* **Exercise 3.2:** Prepare a loop such that given when it is given as an input a string variable it produces as output another string variable equal to the first string in reversed order, e.g. a = "abcd" and reverseda = "dcba".
* **Exercise 3.3:** Use a loop to convert the string "Testing loops and strings…" into another string, changing spaces into "\_" (underscore).
* **Exercise 3.4:** Given the list `AA = [1.0, -2.0, 3.0, 5.5, 0.3]` and considering that these are the values of the coefficients for a polynomial `Px = AA[0] + AA[1]*x + ... + AA[n]*x**n`, prepare a loop that computes the polynomial value for a given independent variable `x` value. For example, in the given case `Px = 7.8` for `x = 1`.
* **Exercise 3.5:** Compute for the loaded temperature dataset the average seasonal temperatures and gives as a results a Python list with these temperatures.
* **Exercise 3.6:** Build a code that, for a given month and considering the loaded temperature dataset, finds the mean and minimum temperature values for the selected month and then it builds a Python list with the years that have an average temperature for the selected month that is less than the average value of the minimum and mean temperature.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://curropb.gitbook.io/python-notes/python-lesson-3.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
