<jupyter_start><jupyter_text>Python Block Course Session 1: Python basics and programming fundamentals Prof. Dr. <NAME>, <NAME> Term 2019 / 2020In this first session we will learn how to use **Jupyter Notebooks** and how to execute Python code. Furthermore, by doing so we will see how basic **functions** are executed, information is stored in **variables** and code is structured with **conditional** and **control flow statements**.   1.1 Python   **Python** is a **programming language** that enjoys great popularity because of **various advantages**. Some advantages are: - Fast to Learn- Easy to Write- Flexible- Extensive Library Support- Hidden Low-Level Aspects of Computer ArchitectureThis makes **Python** an ideal basis for **beginners** like us to start programming because of its **simplicity**. But beyond that it is also used by **experts** worldwide, e.g. in **machine learning** and **software development**, because of its **extensive libraries**.   1.2 Jupyter Lab   **Jupyter Lab** is a very useful **interface** for **Python**. Inside Jupyer Lab you can work with **Jupyter Notebooks**, exactly one like you are looking into right now.  These notebooks are one of the possibilities how to write and execute Python code. The major advantage over others is that code can be **written** very **interactively**, because the entire code can be divided into smaller snippets, and these snippets are directly next to their output. A **Jupyter notebook** consists of separate **text cells** and **code cells**. A **cell** can be **executed** either by clicking on the **play button** in the tool bar on top or by pressing __shift + enter__.If you make a mistake you can always edit the content of a cell and execute it again to update it. You can **insert** additional **cells** by clicking on the **plus button** in the tool bar on top.<jupyter_code>1 + 1<jupyter_output><empty_output><jupyter_text>1.3 Calculations As you can see, you can use Python like a **simple calculator**. You can use the **arithmetic operators** `+`, `-`, `*`, `/`  and **parentheses** `()` just like you would expect for a normal calculator. The Python symbol to calculate **powers** is `**`. Let us give it a try: What is the result of $\sqrt{5-3}$?<jupyter_code>(5-3)**(1/2)<jupyter_output><empty_output><jupyter_text>Although this calculation is very simple, it is always a good idea to write comments about what you are doing. **Comments** in Python start with the **hash character** ``, and extend to the end of the line. They are not interpreted by Python.<jupyter_code># calculate result
(5-3)**(1/2)<jupyter_output><empty_output><jupyter_text>1.4 Built-In Functions A convenient way to get things done in Python is to use functions. **Functions** are indicated by **round brackets** which are appended directly after the **name** of the **function**. **Inside** the **brackets** the **input** is handed over to the function.We have seen above that after our simple calculation the result is automatically shown, but in pratice it is better if the output is **explicitly called** by a **function**. This is done with the **function** `print()`, which prompts all **inputs** on the **screen**. As we can see, the following code gives us the **same result**, but we make clear that we want to get the **result prompted**.<jupyter_code># print result
print((5-3)**(1/2))<jupyter_output><empty_output><jupyter_text>However, the function `print()` can do much more that just prompt simple inputs. It allows you to combine **multiple inputs**, either as **list** of **inputs** or as a **formatted string**.<jupyter_code># print as list of inputs
print("The result is:", (5-3)**(1/2))
# print as formatted string
print("The result is: {}".format((5-3)**(1/2)))<jupyter_output><empty_output><jupyter_text>What else you can do with the function `print()`, you can find out with another function: The **function** `help()` gives you the **purpose** of a function, its **input parameters** and **descriptions**.The **help** for the function `print()` looks as follows, where **value** are the **input arguments** we have given into the function so far.<jupyter_code>help(print)<jupyter_output><empty_output><jupyter_text>If you **do not know** a certain function, or if you are **uncertain**, then please use the **function** `help()` or check it on the **internet**. To name a few more functions besides `print()` and `help()`, these are also **popular functions**:| Function | Purpose || -------- | ------- || `abs()` | absolute value of the argument || `dir()` | list of arguments and methods || `len()` | number of items in a container || `max()` | with a single iterable argument, return its biggest item || `min()` | With a single iterable argument, return its smallest item || `open()` | open file and return a stream || `range()` | produces a sequence of integers from start (inclusive) to stop (exclusive) by step || `round()` | round a number to a given precision in decimal digits (default 0 digits) || `sorted()` | new list containing all items from the iterable in ascending order || `sum()` | sum of iterable of numbers|| `type()` | objects type || `zip()` | tuple where the i-th element comes from the i-th iterable argument |For a **complete list** of **functions** which are **always available** in the Python interpreter, please have a look at the [Python documentation](https://docs.python.org/3/library/functions.html).  1.5 Variables For more complex calculations it is convenient to **store numbers** as **variables**. As a **variable name**, any combination of letters, numbers, and underscores which is not starting with a number can be used in principle. For **readability** purposes, it is recommended to use lowercase words separated by underscores as variable names. The **equals sign** `=` is used to **assign** a value to a variable. Let us try it with variables again: What is the result of $\sqrt{5-3}$?<jupyter_code># define variables
base = 5 - 3
exponent = 1 / 2

# calculate result
result = base ** exponent

# print result
print(result)<jupyter_output><empty_output><jupyter_text>Note, that the `=` in code is similar to the equal sign `=` in math, but it is not quite the same. Python always evaluates **code** on the **right** of the equal sign and **assigns** it to the **variable** on the **left**. So while the first snippet works, the second one does not:<jupyter_code># define variable correctly
base = 5 - 3
# define variable incorrectly
5 - 3 = base<jupyter_output><empty_output><jupyter_text>However, **variables** can also be used on the **right side**. Note, that the right side is evaluated first and then assigned to the variable.<jupyter_code># define base with right side variable
base = 5
base = base - 3

# print base
print(base)<jupyter_output><empty_output><jupyter_text>Here, we took the value of `base`, subtracted 3 from it and then assigned the result back to the same variable name `base`. As a **shortcut** for such an operation we can use the operator for **subtraction assignment** `-=`.<jupyter_code># define base with subtraction assignment
base = 5
base -= 3

# print base
print(base)<jupyter_output><empty_output><jupyter_text>This **shortcut** works for all common **operators** with so-called **assignment operators**: | Operator | Description || -------- | ------- || `+=` | adds right side to left variable and assigns the result to left variable || `-=` | subtracts right side from left variable and assigns the result to left variable || `*=` | multiplies right side to left variable and assigns the result to left variable || `/=` | divides right side from left variable and assigns the result to left variable | As we said earlier, always make sure that your variable names do not start with a number, are written in lowercase words, seperated by underscores and clearly identify your variable. Pay attentation to their upper and lower case letters as well, because variables names are case sensitive in general. Also make sure that none of your variable names conincides with a **set of keywords** which are reserved and have a specific meaning. The **following set** of **keywords cannot be used** in Python as **variable names**:<jupyter_code>import keyword

# print keywords occupied by interpreter
print(keyword.kwlist)<jupyter_output><empty_output><jupyter_text>Exercise: Compute analytically the sum and average of all integers from 0 to 1,000. Use variables for all intermediate results. Print your results.     Exercise: Compute numerically the sum and average of all integers from 0 to 1,000. Use variables for all intermediate results. Print your results.  1.6 Data Types In Python, every variable value has a certain **data type**. Actually every data type is a **class** and every **value** is an **instance** of any of these classes. When we declare a variable, we do not need to explicitly mention the data type. This feature is famously known as **dynamic typing**.We will discuss the **standard data types** in Python step by step. But first let us get an **overview** of them all:| Data Type | Description || -------- | ------- || `Integer` | integer number || `Float` | floating point number || `Boolean` | truth value (either true or false) || `String` | text || `Array` | mutable sequence of values with same type || `Tuple` | immutable sequence of values of any types || `List` | mutable sequence of values of any types || `Dictionary` | associative mapping with keys and values || `Set` | unordered set of distinct values |  Integer and Float Ideally we start with the **numerical types** because we have already got to know them. The first is `Integer` and can hold **whole numbers**, **positive** or **negative**. `Integer` cannot hold decimals. In order use **decimals**, a `Float` must be used. `Float` can also hold positive and negative numbers. Let us try this with an **example**. To verify the **type** of any object, we use the function `type()`.<jupyter_code># define integer number
base = 5

# check type of integer number
print(type(base))
# define decimal number
result = 1.4142

# check type of decimal number
print(type(result))<jupyter_output><empty_output><jupyter_text>Boolean The next data type is of great importance in programming, as we will see with the **conditional** and **control flow statements** today. `Boolean` can hold **truth values**, which are either `True` or `False`. In comparison to the numerical context they behave like `1` and `0`. Let us initialize our **first boolean**. To verify the **type** of any object, we use the function `type()`.<jupyter_code># initialize boolean
difficult = False

# check type of boolean
print(type(difficult))
# initialize boolean
understood = True

# check type of boolean
print(type(understood))<jupyter_output><empty_output><jupyter_text>String We have already seen the next data type too, as we printed text with the function `print()`. In general, **text** can be held in `String`, with the text surrounded either by **single quotation marks** or **double quotation marks**. Accordingly `"Hello world"` is the same as `'Hello world'`. To write a `String` over **multiple lines**, you can use **three quotation marks**.Let us write some **first texts**. To **prompt** the texts, we use the function `print()`. To verify the **type** of any object, we use the function `type()`.<jupyter_code># initialize string
sentence = "I love Python!"

# print string
print(sentence)

# check type of string
print(type(sentence))
# initialize multiline string
sentences = """I love Python!
This is very easy!"""

# print multiline string
print(sentences)

# check type of multiline string
print(type(sentences))<jupyter_output><empty_output><jupyter_text>Due to the fact that `String` is a **sequence** of **characters**, the **individual characters** can also be accessed using **indexing** and **slicing**. Each letter, number, whitespace or symbol gets its own index. With a **positive index** you can access a `String`, where the index starts with `0` from the **beginning** of the string. With a **negative index** you can also access a string, this is especially helpful if you want to access the **ending** of the `String`. In the negative case the index starts with `-1` at the end of the string. Let us have a look at the **index breakdown** of a string.| Character | I | | l | o| v| e |  | P | y | t | h | o | n | !   | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---| Positive Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13| Negative Index | -14 | -13 | -12 | -11 | -10 | -9 | -8 | -7 | -6 | -5 | -4 | -3 | -2 | -1 To access a **single character** within a `String`, we simply take its index and write it in **square brackets** behind the **variable name** where the `String` is stored. Let us try to get the **exclamation mark** out of the previous `String`.<jupyter_code># initialize string
sentence = "I love Python!"

# print character with positive index
print(sentence[13])

# print character with negative index
print(sentence[-1])<jupyter_output><empty_output><jupyter_text>To access **multiple** (but not all) **characters** in a `String`, you can use the same indexes to make **slices**. With slices, a **range** of **characters** is taken from the actual `String` based on their index numbers. Note that the **first character** at the slice start is **inclusive** and the **last character** at the slice end is **exclusive**. For slicing, the slice start and slice end are again written in **square brackets** behind the **variable name**, where both are separated by a **colon**. If you omit the start slice or the end slice, and use a colon, the `String` is simply used from the beginning or to the end. You can do the same with **negative indexes**, but keep in mind that start and end index are **reversed**, and thus also which **character** is **inclusive** and **exclusive**. We will not go into detail here, just try it yourself. Let us try to extract **some parts** of the previous `String`.<jupyter_code># initialize string
sentence = "I love Python!"

# extract inside characters
print(sentence[7:13])

# extract beginning characters
print(sentence[:7])

# extract ending characters
print(sentence[7:])<jupyter_output><empty_output><jupyter_text>Array In addition to the primitive data types that we have dealt with so far, there are also data types which are rather a collection of these. Traditionally, the `Array` is the first **non-primitive data type** that is dealt with. In general, `Array` in Python are a compact way of **collecting basic data types**, all the entries in an `Array` must be of the **same data type**. However, `Array` is not popularly used in Python and not a build in data type, unlike in other programming languages. To work with them you would need to `import` **additional libraries**.In general, when people talk of an `Array` in Python, they are actually referring to `List`. However, when we will work with the `numpy` library, you will see that there are **fundamental differences** between them. But at this point, we will first discuss what a `List` is.  Tuple But before we actually get to `List`, we will discuss another data type that exists in Python. `Tuple` is another **standard sequence data type**. The main difference between `Tuple` and `List` is that `Tuple` is **immutable**, which means once defined you cannot delete, add or edit any values inside it. Due to this property, `Tuple` is mainly suitable for collections which are fixed and will not change. `Tuple` is typed with **round brackets**, `(` and `)`, and its **elements** are separated by a comma `,`. To access certain **values** inside your `Tuple`, you can use their **indexes** again. Let us create our first `Tuple`. To verify the **type** of any object, we use the function `type()`.<jupyter_code># define tuple
points = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

# check type of tuple
print(type(points))<jupyter_output><empty_output><jupyter_text>List A `List` is a data structure that contains **multiple values** in an **ordered sequence**. These are **mutable**, which means that you can change their content without changing their identity. You can recognize a `List` by its **square brackets** `[` and `]` that hold **elements**, separated by a **comma** `,`. `List` is built into Python: you do not need to invoke them separately.Let us create our first list. To verify the **type** of any object, we use the function `type()`.<jupyter_code># define list of decimal numbers
points = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# check type of list with decimal numbers
print(type(points))<jupyter_output><empty_output><jupyter_text>A `List` can contain various **elements** of **different data types**, which can be duplicates and `List` again.Let us look at a **worst-case example** to illustrate what is **principally possible**.<jupyter_code># define list with different data types
messy_points = [False, 1, [2, 3, 4, 5, [6, 7, 8, 9]], "ten"]

# check type of list with different data types
print(type(messy_points))<jupyter_output><empty_output><jupyter_text>Whenever we want to access an individual **element** of a `List` we can do so by typing the **list name** and the **index** of the element in **square brackets**. And if we want to access **multiple elements** in a `List`, then it also remains the same, we can select with **slicing** a start and stop index. Again, indexes start at zero, and may be negative.<jupyter_code># define list
points = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# extract one element
print(points[-1])

# extract multiple elements
print(points[:-1])<jupyter_output><empty_output><jupyter_text>In contrast to a `Tuple`, the **elements** in a `List` can also be **changed afterwards**. How we add and delete elements, we will learn later in the methods section. But we can already discuss here how **elements** inside a `List` can be **changed**. For this, the elements which are supposed to be changed are called with their **index** or **slice** and then assigned to **new elements**.<jupyter_code># define list
points = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# overwrite element
points[10] = 11
points[9] = 8

# print list
print(points)

# modify elements
points[9:] = [9, 10]

# print list
print(points)<jupyter_output><empty_output><jupyter_text>Another characteristic of `List` is that it cannot be simply copied from variable to variable. Only the **reference** where the `List` is saved is stored in the **variable**. For example, if you copy `list1` to `list2`, all changes made in `list1` will automatically change `list2` too, and vice versa. To actually create a new `list2`, you can pass the original `list1` into the function `list()` to actually **force** a new `List`. Let us take a look at a **common problem** with **references**.<jupyter_code># define list
points = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# create new list
new_points = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# modify new list
points[10] = 11

# print original list
print(points)<jupyter_output><empty_output><jupyter_text>Dictionary Like a `List`, a `Dictionary` is a **collection** of **many values**. Unlike `List`, which is indexed by a range of numbers, `Dictionary` is indexed by keys. **Keys** for `Dictionary` can use many different data types, not just integers. It is best to think of a dictionary as an **unordered set** of **key-value-pairs**, with the requirement that the **keys** are **unique** (within one dictionary). A pair of **curly brackets** creates an empty `Dictionary`: `{` and `}`. In them you can add as many **key-value-pairs** as you like, each written in the notation `key: value`, and separated by **commas** `,`. `Dictionary` is exactly what you need if you want to implement something similar to a **telephone book**. None of the data structures that you have seen before are suitable for a telephone book.Let us create our first `Dictionary`. To verify the **type** of any object, we use the function `type()`.<jupyter_code># define dictionary
happy = {"no": [0, 1, 2, 3, 4], "yes": [5, 6, 7, 8, 9, 10]}

# check type of dictionary
print(type(happy))<jupyter_output><empty_output><jupyter_text>If we want to **access** a certain **value** inside a `Dictionary`, then we can access it by typing the **dictionary name** followed by the corresponding **key** inside **square brackets**. Let us try to get the **value** in a `Dictionary` based on its **key**.<jupyter_code># define dictionary
happy = {"no": [0, 1, 2, 3, 4], "yes": [5, 6, 7, 8, 9, 10]}

# print value
print(happy["yes"])<jupyter_output><empty_output><jupyter_text>Just like in a `List`, you can change and add elements in a `Dictionary` afterwards. To change the value of an **existing key**, the position of the **key** in the `Dictionary` can be **overwritten**. We have already seen how this works with a `List`. In addition, **new keys** can be added by simply adding a new key to the dictionary using the **key** as **index** and assigning it to the corresponding **value**. Let us try to **change** an **existing dictionary**.<jupyter_code># define dictionary
happy = {"no": [0, 1, 2, 3, 4], "yes": [5, 6, 7, 8, 9, 10]}

# modify existing key-value-pair
happy["yes"] = [8, 9, 10]

# add new key-valie-pair
happy["not quite"] = [5, 6, 7]

# print dictionary
print(happy)<jupyter_output><empty_output><jupyter_text>As with a `List`, a `Dictionary` cannot be simply copied from variable to variable as well. The reason is that only a **reference** is stored in the **variable**. To actually create a new `Dictionary`, you can pass the original `Dictionary` into the function `dict()` to actually **force** a new `Dictionary`.  Let us demonstrate how to make an **independent copy** of a `Dictionary`.<jupyter_code># define dictionary
happy = {"no": [0, 1, 2, 3, 4], "yes": [5, 6, 7, 8, 9, 10]}

# initialize copy
new_happy = dict({"no": [0, 1, 2, 3, 4], "yes": [5, 6, 7, 8, 9, 10]})

# modify copy
new_happy["yes"] = [8, 9, 10]

# print original dictionary
print(happy)<jupyter_output><empty_output><jupyter_text>Set A `Set` is an **unordered** and **mutable collection** of **distinct** (unique) **elements**. It is useful to create something like a `List` that can only hold unique values. This is particularly helpful when going through a huge dataset. `Set` objects also support mathematical operations like union `|`, intersection `&`, difference `-`, and symmetric difference `^`. Within a pair of **curly brackets** `{` and `}`, you can add as many **elements** to the `Set` as you like, each separated by a **comma** `,`. Note, however, that if you want to create an **empty** `Set`, you do **not** use the **curly brackets** because they will create a dictionary, but you rather call the function `set()` explicitly.In comparison to a `List` or `Tuple`, a `Set` **cannot** be accessed with **indexing** or **slicing** because it is an unordered data type. However, a `Set` can be edited, but this only works with methods which we will introduce later. Let us create a **first set** anyway and show that the elements are **deduplicated** automatically.<jupyter_code># define set
points = set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10])

# print set
print(points)

# check type of set
print(type(points))<jupyter_output><empty_output><jupyter_text>Exercise: Suppose you have to correct an assignment where students can only get whole points from 0 to 10. Define all possible points in the most appropriate data type.     Exercise: Suppose you want to be able to find out students' current assignment scores based on their student ID. Create student Hans, whose student ID is 01/99999 and assignment scores are 9, 8, 10 points. Choose the most appropriate data type.   1.8 Type Conversion Now that we can determine the most appropriate data type for our use case, nothing can go wrong anymore right?Not quite. What we do not know yet is how the **different data types** can be **combined**, e.g. in calculations. What data type can be linked with other data types is mostly a matter of **experience**. But luckily Python offers a big support here: **Implicit type conversion**. This is the **automatic conversion** of Python into the **smallest common data type**, when operands of different types appear in an expression, such that they have the same type. For all other cases, there is the **explicit type conversion**: Using the **functions** for the different **data types**, certain types can be converted into others. Here is a brief **overview** of which **functions** can be called to explicitly convert to the corresponding data type, and with which **data types** it is compatible.| Function | Compatible Data Types  || -------- | ------- || `bool()` | `Boolean`, `Integer`, `Float`, `String`, `Tuple`, `List`, `Dictionary`, `Set` || `float()` | `Integer`, `Float`, `String` || `int()` | `Integer`, `Float`, `String` || `str()` | `Integer`, `Float`, `String`, `Tuple`, `List`, `Dictionary`, `Set` || `tuple()` | `Tuple`, `List` || `list()` | `String`, `Tuple`, `List`, `Dictionary`, `Set` || `dict()` | `Dictionary` || `set()` | `String`, `Tuple`, `List`, `Dictionary`, `Set` |Let us do a couple of **examples** with **explicit type conversion**.<jupyter_code># define float
result = 1.4142

# convert float to integer
result = int(result)

# print integer
print(result)
# define string
result = "1.4142"

# convert string to float
result = float(result)

# print float
print(result)
# define string
sentence = "I love Python!"

# convert string to list
sentence = list(sentence)

# print list
print(sentence)
# define dictionary
happy = {"no": [0, 1, 2, 3, 4], "yes": [5, 6, 7, 8, 9, 10]}

# convert dictionary to list
happy = list(happy)

# print list
print(happy)<jupyter_output><empty_output><jupyter_text>1.8 Methods Besides functions, there are also methods in Python. Unlike functions, **methods** are associated with **certain objects**. The method is implicitly used for a certain object for which it is called. The method usually works on the data of an instance of an object, and returns a desired result accordingly (or not). The **syntax** of methods is the **name** of the **instance object** followed by the respective **method name** seperated by a **dot** `.` Hopefully, the concept of methods will become clear with the help of the upcoming examples. Let us try out some **simple methods**.<jupyter_code># initialize string
sentence = "I love Python!"

# convert string in upper case
sentence = sentence.upper()

# print string
print(sentence)
# initialize string
sentence = "I love Python!"

# replace values by other values
sentence = sentence.replace("Python", "R").replace("love", "hate")

# print string
print(sentence)
# initialize list
points = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# add element to list
points.append(11)

# print list
print(points)
# define sets
points = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
happy_no = {0, 1, 2, 3, 4}

# get difference of lists
happy_yes = points.difference(happy_no)

# print list
print(happy_yes)<jupyter_output><empty_output><jupyter_text>To get a list **all attributes** and **methods** a variable has, we use the function `dir()`.Let us see what other **methods** a **set** has.<jupyter_code># define set
points = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

# print methods
print(dir(points))<jupyter_output><empty_output><jupyter_text>1.9 Conditional Statement So far you know the basics of individual instructions and that a program is just a series of instructions. But the real strength of programming is not just executing one instruction after another. Based on how the **expressions** evaluate, the program can decide to **skip instructions**, **repeat** them, or **choose** one of several instructions to run. In fact, you almost never want your programs to start from the first line of code and simply execute every line, straight to the end. **Control flow statements** can decide which Python instructions to execute under which **conditions**.For this, we have to understand the different types of operators in the first place.   Comparison Operator **Comparison operators** are used to **compare two values** and evaluate down to a single `Boolean`. They evaluate either as `True` or `False`. In Python, you can can use the following comparison operators.| Operator | Description  || -------- | ------- || `==` | equal || `!=` | not equal || `>` | greater than || `<` | less than || `>=` | greater than or equal to || `<=` | less than or equal to |Let us try **comparison operators** out with a **little example**.<jupyter_code># define age
age = 17

# print first comparison
print("I am allowed to get beer:", age >= 16)

# print second comparison
print("I am allowed to get spirits:", age >= 18)<jupyter_output><empty_output><jupyter_text>Logical Operator **Logical operators** are used to **compare multiple** `Boolean`. Like comparison operators, they evaluate these expressions down to a `Boolean`. Since the comparison operators evaluate to `Boolean`, you can use them in **expressions** with the **logical operators**. Note that these operators are often grouped in **smaller statements** with **round brackets** `(` `)`. In Python, you can use the following logical oporators.| Operator | Description  || -------- | ------- || `and` | returns `True` if both statements are `True` || `or` | returns `True` if at least one of the statements is `True` || `not` | returns `True` if result is `False` and vice versa |Let us put **logical operators** in our **previous example**.<jupyter_code># define age
age = 17

# print first comparison
print("I am allowed to get beer and spirits:", age >= 16 and age >= 18)

# print second comparison
print("I am not allowed to get beer and spirits:", not (age >= 16 and age >= 18))

# print third comparison
print("I am allowed to get either beer or spirits:", age >= 16 or age >= 18)<jupyter_output><empty_output><jupyter_text>1.10 Control Flow Statements With the help of **conditional statements**, we can decide which parts of our code are **skipped**, **repeated** or **select** which part of code is executed. To do this, we write **control flow statements**. We will now discuss each of these statements one after the other.   If Statement The most common type of control flow statements is the `if` **statement**. An `if` statement's **clause** will execute only if the **statement's condition** is `True`. The clause is **skipped** if the condition is `False`. The `else` clause is executed only when the `if` statement's condition is `False`. While only one of the `if` or `else` clauses will execute, you may have a case where you want **one of many** possible clauses to execute. The `elif` **statement** is **optional** and is an *else if* statement that always follows an `if` or another `elif` statement. The `if` and `elif` statements' condition are gone through **from top to bottom**, and once a condition is `True`, the respective clause will be executed and the **remaining statements** will be **skipped**. The **syntax** of an `if` **statement** looks as follows.```pythonif condition1:    clause(s)elif condition2:    clause(s)elif condition3:    clause(s)...else:    clause(s)```Let us make an **example** where we **check** our **statements** before we print the results.<jupyter_code># define age
age = 17

# check conditions and print results
if age < 16:
    print("I am allowed to get water!")
elif age < 18: 
    print("I am allowed to get water and beer!")
else:
    print("I am allower to get water, beer and spirits!")<jupyter_output><empty_output><jupyter_text>For Loop The `for` **statement** supports **repeated execution** of a block of code. We also speak of a `for` **loop**. The number of repetitions is specified by the for statement, more precisely by an **iterable object**, e.g. `List`.  We will also discuss shortly what a `break` and `continue` statement is.The **syntax** of a `for` **loop** looks as follows.```pythonfor target in iterable:    clause(s)```Let us make an **example** where we **loop** over **many ages** and print their results.<jupyter_code># define ages
ages = range(0, 19)

# print ages
print(list(ages))

# for loop over ages
for age in ages:
    # check conditions and print results
    if age < 16:
        print("I am {} years old and allowed to get water!".format(age))
    elif age < 18: 
        print("I am {} years old and allowed to get water and beer!".format(age))
    else:
        print("I am {} years old and allowed to get water, beer and spirits!".format(age))<jupyter_output><empty_output><jupyter_text>Break Statement If inside a `for` loop the execution reaches a `break` **statement**, it **immediately exits** the `for` loop's clause. The `break` statement is only allowed inside a loop body. It should also be noted that when you have multiple nested `for` loops, only the **inner-most nested loop** will be exited. A common reason to use a `break` statement is when a certain point has already been reached before the `for` loop finished. Let us make an **example** where we **exit** the **previous loop**.<jupyter_code># define ages
ages = range(0, 19)

# print ages
print(list(ages))

# for loop over ages
for age in ages:
    # check conditions and print results
    if age < 16:
        print("I am {} years old and allowed to get water!".format(age))
    elif age < 18: 
        print("I am {} years old and allowed to get water and beer!".format(age))
        break
    else:
        print("I am {} years old and allowed to get water, beer and spirits!".format(age))<jupyter_output><empty_output><jupyter_text>Continue Statement If inside a `for` loop the execution reaches a `continue` **statement**, it **immediately exits** the **current iteration** of the loop body and it **continues** with the **next iterations** of the loop. The `continue` statement is only allowed inside a loop body. It should also be noted that when you have multiple nested `for` loops, only the **inner-most nested loop** will be continued. A common reason to use a `continue` statement is when a certain point has been reached when the result already cannot be reached inside this interation that the rest of it can be skipped.Let us make an **example** where we **skip** some **iterations**.<jupyter_code># define ages
ages = range(0, 19)

# print ages
print(list(ages))

# for loop over ages
for age in ages:
    # check conditions and print results
    if age < 16:
        continue
        print("I am {} years old and allowed to get water!".format(age))
    elif age < 18: 
        print("I am {} years old and allowed to get water and beer!".format(age))
    else:
        print("I am {} years old and allowed to get water, beer and spirits!".format(age))<jupyter_output><empty_output><jupyter_text>While Loop In the `while` **statement**, the condition is always **checked** at the **start of each iteration**. That is, each time the loop is executed. We also speak of a `while` loop. If the condition is `True`, then the clause is **executed**, and afterward, the condition is checked again. The first time the condition is found to be `False`, the `while` clause is **skipped**. If the execution reaches a `break` statement, it **immediately exits** the `while` loop's clause. When the program execution reaches a `continue` statement, the program execution immediately **jumps back** to the start of the loop and reevaluates the loop's condition. This is also what happens when the execution reaches the end of the loop.The **syntax** of a `while` **loop** looks as follows.```pythonwhile condition:    clause(s)```Let us make an **example** where we **loop** over **many ages** and print their results.<jupyter_code># define ages
age = 0

# for loop over ages
while age < 19:
    # check conditions and print results
    if age < 16:
        print("I am {} years old and allowed to get water!".format(age))
    elif age < 18: 
        print("I am {} years old and allowed to get water and beer!".format(age))
    else:
        print("I am {} years old and allowed to get water, beer and spirits!".format(age))
    # increment age
    age += 1<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11909702049962652, 'fraction_numerical': 0.012808808476498741, 'mean_word_length': 4.653425086018142, 'pattern_counts': {'":': 12, '<': 164, '<?xml version=': 0, '>': 161, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '37', 'dir': 'jupyter-structured-clean-dedup', 'id': '195350', 'license': 'Apache-2.0', 'n_tokens_mistral': 10335, 'n_tokens_neox': 9686, 'n_words': 5175, 'path': '1-basics/1-session.ipynb', 'repo_name': 'stefan-scholz/python-block-course-2019', 'size': '36168'}