 Welcome back to the third chapter of the Introduction to Python and Programming Course. This chapter is on conditionals and also on exceptions. So let's start with something simple. We have seen before how Python returned values that are read like true and false. And what these usually mean is yes and no. And we have a technical term for such expressions. We save it any expression that returns either a true or a false, a boolean expression. So it's basically a true or false statement about something. But the term statement again is a little bit to be used with caution because we are really talking about expressions in the syntactical sense. But it's about a yes or no answer to some question we might have. So let's start simple. I have two 42s here that I compare to each other. Another question is what does Python do in memory? And let me quickly put that on a memory diagram so that when I execute a cell, it's of course true. We have seen such cells before. But now we want to understand what Python really does in memory. I want to teach it in the way that you should think about objects being compared to each other. So when Python reads this, it evaluates the left-hand side first and it creates an object, the number 42, it's of type int, and then it evaluates the right-hand side and it happens to be another object. And we also write in there 42 and it's also of type int. And then Python goes into the middle and sees there is the comparison operator, the double equal sign. And then Python goes ahead and asks the double equal sign, hey, what do you want me to do? And the double equal sign dispatches this question to the two objects that are compared. So basically the double equal operator here will go to the left-hand side, to the left object, to this one here, and will ask to this object, hey, you 42 object, can you compare yourself to the other object? And then the left integer object looks at the right integer object and basically says, well, yes, the other object is an integer. So I know how to compare myself to the other object. So it goes ahead and does it. And then the left object finds out that both integer objects have the same value inside, the same ones and zeros. And then the left object returns to Python and says, well, we are equal. And then Python says, OK, if the objects confirm to me that they are equal to each other, then I will give back the true here as a return value. The same holds true for the next statement, for the next expression where I compare 42 to 123. And Python would also go ahead and create those two objects and then ask the left-hand side first, hey, 42 object, do you know how you can compare yourself against the 123? And it does so. And then it says, no, we are not equal. And then the comparison operator returns false. So you may think that why is Python doing this? Why is Python not comparing the two numbers directly? Why does it have to go through the object and ask the object to compare itself to the other object? Well, there is a reason for that. And the reason for that comes, we can see why this is good with the next example here. So let me compare the integer 42 with the floating point number 42.0. So what then happens is, Python starts on the left-hand side. It creates a new 42 object, so this one here, for example. And let's imagine that the right-hand object is gone. So the left-hand object would be created. And then it would evaluate the right-hand side. And it would create a somewhat bigger object, writes 42.0 in it, labels it with the float type. And then it goes ahead and asks the 42 integer on the left-hand side, hey, can you compare yourself to the float? And then the integer looks at the float and says, well, I don't know what these decimals mean, because I don't know what a decimal is. And so the integer object answers to the comparison operator, hey, comparison operator, I don't know how to compare myself to the float. And then the comparison operator says, well, no problem. I will ask the object on the right-hand side if it knows how the two of you compare. So then Python returns to this object here, the 42.0, and asks the float, hey, float, can you compare yourself to this integer here? And then the float says, well, this is an int object. It does not have any decimals. However, I know that if another number does not have any decimals, that is just as if it were all 0s, because the float type knows how to deal with decimals. And so the float looks at the integer as well, 42 is kind of like 42.0. And then it compares this value against its own value and then says, well, we two are equal and returns to the comparison operator. And then the comparison operator says, OK, the two are equal. So I return a true here. So this is what's going on here. This is basically an example of object orientation that is at hand here. And we will understand fully why this is the case only after studying chapter 10, which is the object that puts everything from chapters 1 through 9 together and introduces the object-oriented features of the Python language. But for now, we just understand that the operators, they don't actually know anything about the objects. All the operators do is they talk to the objects and ask the objects to do something for them. And then in a way, they dispatch the work to the objects themselves and then the two objects to the work and come back to the operator and tell the operator the outcome and then the operator tells the outcome to us. This is the way this works here. And then there is something interesting. So I can compare the number 42, the integer 42 to 42.000 and then I add a 1 here. And for us humans, this should be false, right? However, for a computer, this is true. And now you could think that, oh, maybe this is a bug, but unfortunately, that is not a bug. This is totally on purpose what we are seeing. And we will fully understand why this is the case after studying chapter 5 on numbers. But for now, it's official that we summarize this slide here by saying that the operators dispatch the work to the objects. And the comparison operator in particular is an operator that compares the values of the two objects, of the left object and the right object. And it compares it in a way that I would say we humans think of when we would do the comparison. So in a way, the 42 and the 42.0, they are totally different ones and zeros in memory. So remember that the decimal, of course, needs some other structure of the ones and zeros inside it to model the decimals. And so these, the contents of those two objects here is definitely not the same. However, the meaning of these ones and zeros for us humans, the semantic meaning, is the same, which is why the operator compares equal here. Okay. So we have seen here four examples of Boolean expressions, meaning we get back a true or a false. And now the question is, what are true and false? Well, these are objects. And these are objects of a special type, which is called the Boole type. How can we see that? Well, we can type the word true with an uppercase t here into a code cell and execute it. And that means Python literally understands this. In other words, the word true with an uppercase t is a so-called literal. And the same holds true for false as well, but I don't show it here because true and false is really two sides of the same coin. And then I can ask the question, is true an object? And the answer is, of course, and by now you should guess that is, of course, yes, because everything in Python is an object. That means it has an ID or a memory address, and it also has a type. That means, in other words, when Python starts, so these two numbers here, they are now forgotten because we didn't really store them away so they get forgotten. So when Python starts, remember in the last chapter in chapter two, I said that Python creates some objects already when we started, which model the functions, the built-in functions, and the built-in constructors and so on. And in the same way, Python does the following. Python has a true, which is of type bool, and it has a name, which is also true, and the name references the object. And similarly, we have a second object here, which has the value false. And it's also of type bool, and it goes by the name false. And of course, we have a reference here. So just as Python puts the built-in functions as objects into memory, it also puts objects for the true and false objects into memory when Python starts and also registers these names here. OK, and now remember from chapter two when we defined functions. We said in the chapter that if we specify either no return statement within a function or we do specify a return but don't specify an expression after it, the implicit return value of function will be the none type or the none object. So let's see what is none. Well, none. I can just enter it and just execute it to cell, and I don't see any output. And that is intentional. So whenever none is the return value, the output, then Jupyter Notebook doesn't show it because otherwise we would see none all over the place. And that's actually the reason why we don't see any output sometimes. Whenever the return, whenever the value to which an expression evaluates is none, then we don't see any output here. So what is none? It's, of course, a literal because we can literally type it here and the name exists. So in other words, we must have a name called none here. It must exist. Otherwise, we couldn't type it here because we didn't define it. And then, of course, none also has an ID and also has a type. And the type is the so-called none type, which means that somewhere in memory, there is also an object with the value none. And I just write here NT for none type. And the name none also references this object. So when Python starts, we actually have those three objects here. Okay, let's talk about what can we do with or how can we get what's another way of getting truth and false? Well, we can go ahead with the example where we use the comparison operator. And we can show some or look at some other operators. And the family of operators to which the comparison operator belongs, they are called the relational operators. So far, we have in chapter one, we have seen the arithmetic operators. And these here are now a second set of families, which is called the relational operators. And of course, what goes there is the comparison operator, which we have now seen before. But then, also we have the operator which has a exclamation mark and then an equal sign. And what this means is it means it's not equal to. So it's just the opposite of the comparison. This is the not equal operator, so to say. So when I compare 42 to 123, the answer is called false. But when I ask Python, hey Python, is 42 not equal to 123? The answer is, of course, true. So this just negates the other operator. Some other programming languages, for example SQL, if you happen to know it, they use an operator that looks like this instead. But in Python, this does not work. In Python, it's the exclamation mark and the equal sign. Okay, so what other operators exist? What other relational operators exist? Well, of course, we can compare to numbers with the smaller than operator. And if there is a smaller than, then there should also be a smaller or equal to. And similarly, we have a greater than and a greater or equal to operator here. So this is nothing fancy here. I think you have already figured out that they exist. And that completes now the set of all the relational operators. Now comes another set of operators, which we call the logical operators. Note, these are different than the relational operators, of course. But they always go together oftentimes. So what logical operators do is they connect several Boolean expressions. And we have seen Boolean expressions in the first section of today's chapter. So any expression that returns either a true or a false is a Boolean expression. And whenever we want to connect several Boolean expressions together, we use logical operators. So let's do some examples here. I set A to 42 and B to 87. So maybe I just use our memory diagram here. And this is A. And then we have 87 lose our diagram here as just a little reminder so that we have the 42 and the 87 here. So that we don't forget the two numbers when we go through the examples here. So set them to A and B. And now I could ask the question, for example, is A greater than 5? And at the same time, is B smaller than or equal to 100? And since A is 42 and B is 87, both Boolean expressions on the left-hand side and the right-hand side return true. And then in the middle, we see the so-called logical and operator. And what the end operator does, it takes two operands. It's a binary operator. And it looks at the sub-expression on the left-hand side and the sub-expression on the right-hand side. And only if both sides evaluate to true, then the overall expression will be true. So in this case, the overall expression will indeed be true. Now sometimes people confuse the order of precedence here or they actually get confused. So maybe some people read it like this, that maybe the 5 and B should go first somehow. But the rule is this, the relational operators have a higher precedence than the logical operators. So the second code cell that I write here with the parentheses for grouping is actually the exact same cell as above. So when we type the cell here above, Python evaluates as if we also set the parentheses here. And sometimes, even though it's not necessary to use parentheses, it makes the code look more readable. And then now we have seen the end operator. And there are two more. There is the logical or operator here. And then there is also the logical not operator. And both of them are rather trivial. So the simplest one of the three is the not operator. The not operator, what it does is it takes one operand, which is on the right-hand side. So and then here the right-hand side evaluates to false because B is 87 and 87 is not greater than 100. And so this is false here. And then the not operator will just flip the truth value. So a false becomes a true and the true becomes a false. So taking this all together on the right-hand side, this will become true. And then we have the all operator that connects the left-hand side and this here. And the left-hand side 42 is not smaller than or equal to 5. So this would be false. And then we have a false on the left-hand side and a true on the right-hand side. And they are connected via the all operator. And the all operator has the rule that if either side is true or if both sides are true, then the overall result will be true. And because we have a false on the left-hand side and a true on the right-hand side, that means the overall result will be true. So here it's not so clear which of the two operators goes first. I just did it the right way. And I said that the not operator goes first. So the not operator has a higher precedence than the all operator. But we could, of course, use parentheses like this to be more precise here. And we can actually even overdo it in a way when we put in even one more layer of parentheses. So the cell up here is actually equivalent to the cell down here. So basically that means we evaluate these parentheses first, and then we evaluate these parentheses here. And then we evaluate these parentheses. And then only at the end, we connect the left-hand side and the right-hand side here. And the order of precedence in total is like this. The highest order of precedence has the not operator. Then comes the and operator. And then the all operator has the least precedence here. So far, so good. I think everything is rather trivial. And now comes a nice extension that Python offers, a nice feature. So let's say we have an expression like this, where I say 5 is smaller than a, and a is 42. And at the same time, a should be smaller than 87. And whenever we see that we have the same operator on the left-hand side and the right-hand side, and we have in between them the same operand. So we have an expression here. And we have the same expression here, and they are connected via an end. Whenever we see this, what we can do is we can change the operators. So we could write 5 is smaller than a, smaller than 87. And this will be exactly the same. So the lower code cell here is just a shorter version of the upper code cell here. And yeah, this does not work in too many languages. In Python, it works. And this is one reason why Python, when it's modeling mathematical problems, looks very nice because this way, by using the second notation here, the code will actually look closer to the math than the code up here. Because in math, we would actually use to do the chaining when writing some notations. And now comes something that is a bit specific to Python. So we have now learned what are true and false, the Boolean types. And so there is also an expression that a Python programmer use, which is called truthy and falsey. And what this means is, this means that we can actually use a non-Boolean expression. So an expression that does not return true or false in a Boolean context. And what does it mean to use something in a Boolean context? Let's look at an example. So for example, we have a code cell that looks similar to the code cells we saw before. So on the left-hand side, we have A minus 40. And on the right-hand side, we have B is smaller than 100. Well, the right-hand side is obvious, right? B is 87, so 87 is smaller than 100. So the right-hand side is, of course, true. But what is the left-hand side? Let's maybe comment out some part of the code cell and execute the code cell with just A minus 40. And this gives us 2. And that's clear, because A is 42 and minus 40 would be 2. So the left-hand side is obviously not a true and also not a false. It's not a Boolean expression. It's an arithmetic expression, so to say. And we get back A number 2. And what happens is I use the logical end operator and I connect the 2 on the left-hand side with the true on the right-hand side. So let's do that. We get back true. So why is that? This has to do with the fact how Python treats numbers when numbers are used in place of a Boolean expression. So the rule is this. Whenever a number is equal to 0, then it is considered false in a Boolean context. However, if the number is not equal to 0, it is considered true in a Boolean context. So because we have a positive 2 here, this is considered true. So basically, the expression reads true and true. And then if both sides of the logical end operator are true, then the overall expression is true as well. And we can verify that with the Boole constructor. So I can pass A minus 40, which is 2, to the Boole constructor. And the Boole constructor will give me back a true. So you remember what constructors are. We have seen the int constructor before, the float constructor, the string constructor. And usually what constructors do is we can give them an object of any type. And whenever the constructor is able to cast the given object as the type that it resembles, then it goes ahead. And the Boole constructor just takes any object and casts them as either a true or a false. And for numbers, the rule is any number that is not equal to 0 will be true. And any number that is 0 will be false. So one caveat here, let's actually prove that. So A minus 42, this would be 0, right? So the Boole constructor gives us back a false. And here is the caveat that I was talking about. So A minus 43, this will give us negative 1. And many people think that just because the number is negative, it should evaluate to false. But this is, of course, a false friend, so to say. This will give us back a true as well, because negative 1 is also not equal to 0. So negative value may be confusing at first, but they are just as true as our positive numbers. So this is numbers in the Boolean context. And let's look at some other objects in the Boolean context. For example, the none type. So remember that none is returned by functions if we don't specify a return value explicitly. And so what is the Boolean value or the Boolean, what would be none in the Boolean context? So let's call Boole with none, and it is false. And this is something that gets many people confused as well. So whenever you use none in the Boolean context, it acts as if it is false, but it's not false, right? So it's basically it's not the false object simply. It's another object. And maybe some people think that just because the object is called none, it means the absence of a value. This is usually what none means. But at the end of the day, it's not a maybe. It's not an unknown, because people often try to interpret the none as kind of like unknown. So true means yes, false means no, and none means either maybe or maybe unknown. So these are the two interpretations that people use for none often, but they are not correct. So in the Boolean context, none means false simply. This is also a false friend. And now let's look at the third example of objects in the Boolean context. So let's look at an empty list. So an empty list is considered false in the Boolean context. However, if I have a list that contains one object and the object happens to be false, we would maybe think that this should also be false, right? But it is not. It is true. And the rule is simple. Whenever a list is empty, it is considered false. And whenever a list is not empty, it is considered true. That's the rule here. And so no matter what is in the list, even if we put false inside the list, it is still true because it has an element in it. And in the same way, an empty string would also be considered false. However, a string that has some text in it is considered true. So here, the rule is just as the rule is for lists. And an empty list or an empty string is false. And a non-empty list or the non-empty string, they are true. OK, that was a truthy and falsey. OK, and why is the chapter called truthy and falsey? Well, whenever something evaluates to true, but it is not true, so it is not a bool true, but it still evaluates to true, then we just call it truthy, to show that it's actually not true, but it behaves as if it were true. And falsey means an object that behaves as if it is false, but the object is, of course, not false. OK, another topic regarding logical operators is short-circuiting. So what is short-circuiting? So if I look at the expression 0 or 1, so we learned that OR should actually evaluate the left and the right operand as a boolean expression. But here, both the left and the right-hand side, they are not booleans. So what happens? So if I just type 0 or 1, let me execute this first, I get back a 1. So what's happening here? So what is happening? Python interprets this expression only from left to right. So we start with the 0, and the 0 behaves as false, because the 0 is just 0, so it's falsey. And then because the OR receives its overall value from the fact that how is OR interpreted? Well, the rule was if anyone, so either the left or the right side is true, then the overall expression is true. So if the left-hand side is basically a falsey, then what that means is we don't know yet what the overall truth value will be. So we have to evaluate the right-hand side, too. And then the right-hand side, in this case, is 1. And something has to be returned here. And 0 cannot be returned because it is falsey. And because of that, the 1 is returned. But the important point is it's not true or false that is returned. It is the 1 that is evaluated here, the 1 that is created here. This is the thing that gets returned. And let's look at another example to get the point. So now we look at 1 or 2. And let me check what happens when I evaluate this. When we evaluate this, we get back to 1 as well. And the reason for that is because we evaluate from left to right. And if the left object is truecy, it is immediately returned. And why is the left object immediately returned? Well, after the left-hand side evaluates to true, we know what is the value of the overall expression. Because if the left-hand side is true, I don't have to look at the right-hand side, because I know the overall expression will be true, no matter what. And because of that, the left-hand side will be returned. And then I can chain the operators. So I can type 0 or 1 or 2. And the rule for ORS, but now we have figured it out, the first object that evaluates to true, or the first truecy object, is the one that is returned. So this should return 1. And indeed, it does return the 1. And then one more example. Now we have three objects that all are falsey. So we have false to begin with. And then we have an empty list and a 0. And they all are false or falsey. And when in a chain of ORS, they are only false objects, then what happens is, of course, the last one is going to be returned. So we get back to 0 here. So how can you use this observation in your own code? Well, usually what we do is the left-hand side is often the result of, let's say, a user entering something or something that we obtained from some other source. And the right-hand side of the OR is usually a default value. And then what we use this for is to always have a default value for an expression that we want to use. So we always evaluate the left-hand side first. And if the left-hand side is not truecy, we will have a right default value, so to say. So this is often what it is used for. So now let's look at the end operator. So on the left-hand side, we have a 0 and the right-hand side a 1. And the rule here will be how many operands do we have to evaluate until we know the overall truth value. And we know the overall truth value of an end is only true if all the operands are true. And because the left operand is not true, that means we can actually stop evaluating this expression here after the 0 because we know it can never be true. The overall expression can never be true if the left-hand side is not true. And because of that, the 0 is returned. So we can already guess what the rule is for the end operator. The rule will be the first object that evaluates to false is the one that will be returned. So let's check. Let's verify this rule. And now the rule is 1 and 0. So the 1 evaluates to true in a Boolean context. So we have to check the right value because in order to determine the overall truth value, we have to check if all the operands are true. And if the left one is true, we still don't know anything about the overall error. We have to continue. That means we have to evaluate the second operand here. And this is the one that gets, of course, returned here. And then let's be a bit more tricky here. The chained operators here 1 and 0 and 2. So the overall expression will be true only if all three operands are true. And now on the left-hand side, we have a one which is true. So we have to continue to check the remaining two operands if they are true or not. And then the second operand will be false. And because the second operand is false, we don't have to evaluate the two because it doesn't matter if two is true or false because in the middle, if once we get a falsey object, the overall result of the end chain here will be false. And that's why also here, the zero is returned. So again, the rule is the first object that is falsey will be returned. And if there are no falsey objects, like there is a one, two, and three, then what that means is we evaluate the first object, the one, if it's truey, so we have to continue to the two. And then we evaluate the two. The two is also truey, so we have to continue. We go to the three and we evaluate the three and independent of what the three is, the three could be three or zero. So if I execute this, I get back to three. If I put a zero here, I would get back a zero as well. So once we hit the last operand, it doesn't matter what the operand is anyway. So we just get the operand back as it is. Okay, this is short circuiting. And now let's go ahead and put the things together. So we have seen if statements before, but now we will officially introduce it. So what's an if statement? I'll give you an example. So the example goes like this. I have my list numbers consisting of the 12 random numbers or the 12 numbers from one to 12. And I create the list and then I import the random module because I wanna do some random drawing here, basically. And then the task is this and we start with a wrong example, as you can see, but we have to figure out why it's wrong. So the example is this. Given the list of numbers, we will draw a random number out of this list. And then we want to know if the randomly drawn number is divisible by two, divisible by three, maybe it's divisible by both two and three or it's divisible by neither two or three. Okay, so let's look at code. And as you can already see, this first example will actually have a bug and we have to figure out what the bug is. So here's the code. And in the first line, we use the choice function in the random module to draw a random number from the numbers list. And then what we do in the first line of the if statement is, okay, first of all, maybe I should talk about what the if statement, how it works. So we have seen a single if line before in the example in chapter one. So the if statement goes like this. We have an if to begin the statement with and then we have some condition and then we have some body that is executed only if this condition is true. And then if this condition is not true, I have an elif clause here and then another condition is checked. And if this condition is true, then we execute this body of code here. However, if the second branch is not true, the second condition is not true, then we will check the third condition here. And if that is true, we'll execute this line of code. And only if none of the three if and elif clauses are true, then we will execute the code in the else clause here. So regarding the names, as I said, these are the conditions here. There is a one if statement to begin the if statement with. It has a condition, it ends with a colon. And then we have as many elif clauses as we want. They also have a single condition here and also end with a colon. And then we have a so-called else clause and the else clause will basically be reached whenever none of the clauses that come before are reached. So this is like a catch all thing here. So this code will always be executed if none of the conditions were true. In other words, what we can say about this if statement is that in all the times, exactly one of the four clauses will be executed. So it will be either this print function, this print function, this print function or this print function. So exactly one of those four will be executed. And so now let's check the conditions. So here I modulo divide number by two, the random number, and I check if it has no rest. In other words, if this condition is true, I know the number is divisible by two. Then in the second branch, I will modulo divide number by three. And if this is the case, I print out this number is divisible by three. And in the first two branches, I say the number is divisible by two only or by three only. And then in the third condition, I check if the number is divisible by two and if it's also divisible by three and then I will print the message, the number is divisible by two and three. And then for else, I don't need a condition. So let's go back to the headline. This basically reads, so this is basically a one to one translation of the task here into code. So the code says, is the random number divisible by two, three, both or none? And so we have two, three, both and none here. So I just basically translated this task one by one. Now there is a bug in it. I told you that already. So the question is, what is the bug? So in order to find it, maybe let's execute the code cell a couple of times. It's a randomized code cell. So we have to probably do several runs here to see the bug. Okay, let's go on. So first number that is drawn is a 10 and 10 is divisible by two only, that is correct. Then we have a nine. Nine is divisible by three only, that is also correct. Then we have a five, and five is divisible by neither two nor three, that is also correct. And it says one is neither divisible, that's also correct. 11, neither, yes. Three we already saw before. And now here's a bug. Here it says six is divisible by two only, but we know that six is divisible by both, two and three. So what is the bug here? Well, the bug is quite simple. When we draw the random number here and it becomes six, and then we check the first condition, and if we check, is numbers divisible by two perfectly divisible? Then the answer to this is true, and then we go into this branch here, and then all the other branches are not executed. In other words, the problem with this code is that the order of the conditions is not good. In fact, it's wrong. So what we should have done is we should have tested this condition first up here. Okay, so let's correct this code. So here is the same example again, but now with a correct logic. And I put the condition checking for both the numbers first. So maybe, maybe let's go ahead and quickly draw a picture to see how we can, how we should think of this example of what the best order is. So let's say in this circle, this circle is just the set of all the natural numbers. And now in theory, the set is infinite, but here I just draw a finite circle. And now I want to draw in there the other numbers that are divisible by three and also all the numbers that are divisible by two. And then of course, at the end of the day, also all the numbers that are divisible by both. So how could I do that? Well, maybe let's use another color. So maybe let's draw a line. How many numbers are there that are divisible by two? Well, it's probably exactly half. So the numbers up here are divisible by two. And we know it must be half the numbers because half the numbers in the set of all numbers is even and the other half is not. And then the question is, how many numbers in the set of all natural numbers are divisible by three and where are they? So I would suggest that out of all the natural numbers, only one-third of the numbers is divisible by three. And it turns out that we could draw them in like this. And there is an intersection here where these are the numbers that are divisible by two and by three. So now the question is, what do these numbers have in common here? Well, if a number is both divisible by two and also divisible by three, then we know for sure that the number must be divisible by six. So if both conditions apply, then the number must be divisible by six. So let me maybe put in some stars here. So this is a subset. And we see that the set of all the numbers divisible by two and the set of all the numbers divisible by three has an intersection, but they also have an area where they don't intersect. And then we also have some numbers that don't fulfill neither of these conditions. What would be a number here? Maybe the example would be number seven, for example, or the number one. So one would be in here seven and many, many other numbers. And so now let's see how we can use this graph here or this picture here to reason about how we should design our program. So we have what we have to do because only one branch is executed. What happens is we have to check for the most special case first. And the most special case is of course this one here where both conditions are fulfilled. This is why we have to put this condition as the first condition. And then since the condition, a number is only divisible by two and another number is only divisible by three since there are no further intersections between those sets. What that really means is the next two conditions, the next, the ELF statement, the ELF lines here, they could actually be written in any order. It wouldn't make the program buggy at this point. And then of course also after we know that a number is neither fulfilling any of the three conditions, we know that for sure that we must have this catch all here where nothing, when none of the three above applies. So this code is now correct. And let's check this. So 12 is divisible by two and three, that looks good. And we know that the number six was the case that went wrong. So let's maybe go ahead until we find or until we hit the first six and takes a while because the numbers are drawn randomly. And now here we have six is divisible by two and three. So now the code is correct. So just keep in mind, what do we take from here? Well, what you should take from here is the ELF statement, I also called the compound ELF statement, compound statement in Python means it has more than one branches. And we know that the ELF statement always starts with an ELF header line and there has to be one. And then there is a basically an arbitrary number of ELF header lines. So we can have as many ELFs as we want. We could also have none, no ELF. And we can have an ELF clause here. We don't have to, but we can. So this is the maximum that is possible. There is an if, an ELF, and an ELF. And also we learned from this example that only one of the conditions can be true and only one of the branches is executed. So even if, let's say, even if all three conditions are true, then only the first branch would be executed. So what we also take from this is that the order of the condition checking matters. And this is what the example here also shows. And then let's make the code a little bit nicer. I always try to teach you best practices here. So how can we make this code a little bit nicer? Well, I told you already in the diagram here any number that is divisible by two and three is always divisible by six. Or basically it is exactly then it is divisible by six if both of those conditions are true. So when we know about a number that is divisible by two and three, the right-hand side follows. But also if we know that a number is divisible by six, the left-hand side follows here. So a number that is divisible by six is always also divisible by two and three. So what we do here is instead of testing if the number is divisible by three and two, we just unify this into one expression here, into one relational expression. And why is this better? Well, this is better because at runtime, now the computer only has to evaluate one condition and not two. So this is of course in this example, not much of an improvement, but at least the code is a little bit shorter. And also we communicate, I think, a little bit clearer. And also what we see here is a nice pattern. We sort the conditions in a descending order with regard to the divisor here. So this is also to verify the correctness of the program. Okay, and of course, if we test this function or this code here, it is still correct. So that's the if statement. A little bit more info on it. So only the if clause is mandatory. So here is the random function from the random module, which returns a uniformly distributed random number between zero and one, which we often use to model a coin toss. So if I execute the cell here repeatedly, then sometimes we see the text and sometimes we don't. And if we do that often enough, we'll see the text in 50% of the cases here. And then also we can also just leave out the elif clause and then the if statement is just an if else statement. And what I do here is, in this example, I draw a random number again and then I check if the number drawn is even or odd. That's it. Okay, so two is even, seven is odd, two is even, seven is odd, four is even and so on. So it works, the code is correct. But now just to reiterate some point, here we have the arithmetic expression and then the arithmetic expression, the number that is returned here is compared to this with the relational operator here. And this is a boolean expression because it returns a true or false. And now we saw that we don't really need a boolean expression, we can use a number. So if we divide number by, moderately divided number by two, we will definitely get back a number here. And we know how numbers behave in a boolean context. This we know from the chapter on truthy versus falsey. So maybe let's exploit this a little bit and rewrite this code cell. So what we do here is we just copy paste the code and we just leave out the comparison to zero because we don't really need it. Number divided, moderately divided by two is either zero or one. And now we have to be careful. If number moderately divided by two is zero, then the number is even. However, if the number is zero, it will evaluate to false, it is falsey. So what we have to do here is we have to actually exchange the order of the two clauses here. So the print number is even, that was here the first clause. Now it becomes the second clause here. And this is a computationally a little bit better than up here because up here, we have to evaluate two operators and here we have to evaluate only one operator. However, again, this code is perfectly fine in the program as well. So this is not a huge difference here. I'm just reiterating the point that sometimes we can actually leave out comparison here and we can just exploit the fact that Python knows how to treat basically any object in a Boolean context. Okay, and now let's do one more example regarding the if statement. So of course, if statements may be nested, so what does that mean? That means if we have an outer if statement, here we have an if and an else clause, we can put another if statement inside it. And then what happens is we have two levels of indentation and we have four different cases. Now I called this example a hard to read example. So why is this hard to read? Well, I didn't tell you what this example is modeling. So let's figure out what this example must be doing by just reading the code. But first we get a random number again and then judging by the comment, we seem to be tossing a coin. And if the coin is heads, we do something. And if the coin is tails, we also do something. And now we see something that is quite bad actually. We have repetitive code. So inside both the cases, both the heads or the tails case, what we do is we check if the number is even or odd. Okay, so we have the exact same logic within both the branches. And in the case when the number is even, we print out either number is even or we print out can be divided by two without a rest. And if the number is odd, then either we print out is odd or we print out the number divided by two results in a non-zero rest. So what is this actually modeling? Well, the example I had in mind was assume, this is something that we would call AB testing in management. So let's say I have a customer who visits my website and I want to check if some customers or if a customer is more likely to convert to a product if I give him an easier to read message. So the messages in the tails case on the lower end the number is even or the number is odd. They are rather easy to understand for anyone. And then the sentences, the number can be divided by two without a rest and the number divided by two results in a non-zero rest. They have the same meaning, but we have to think a bit if that is the same. So in other words, the heads case, the upper case is basically a more sophisticated message. Like let's assume that's our current message on the homepage and our marketing team thinks that the message is quite bad to understand and we want to try out an easier message for our customer. How do we do that? We do AB testing and what is AB testing? AB testing means half of our customers get one message. The other half of our customers gets another message and then we see which customers have a better conversion rate or something like this. So that's what the example is about. So how long would you have taken to guess what the example is about? Just by reading the source code probably you wouldn't have guessed what this is about. And the reason is, well A, if I don't give you context then it's rather hard to guess what an example is about but also the code is really not nice. It's really hard to read, it's unnecessarily verbose and it has some repetition that I don't like and yeah, it's not the best code. So let's check if the code works at least. So here it's also a randomized code. So two can be divided without a rest, 12 is even, seven is odd, four can divide it without a rest, 10 is even, 11 is odd and then 11 divided by two results in a non-zero rest. So at least the code works, it is not buggy but again it's rather hard to read our code. Let's see how we can improve this. And what we will do here, we will rewrite the code to be a little bit more self-explanatory. So we will still draw a random number in the first line and then we set two variables. The first one is called coinus-heads and coinus-heads is the result of the coin toss here and then we have a second variable called number is even and it is just the result of modulo dividing number by two and comparing it to zero. Okay, so now we do some calculations and we give it a name in form of a variable. That's a nice improvement. If we look back in the old example, we actually needed comments to basically say, tell the user what we are doing and here the inner logic is not even commented. So we don't really know what's going on. We would have to guess from the output from the print, from the output within the print function what we are actually doing here. We don't really know from just reading this line. But now in the easier to read example, I give names to the steps. So this code is easier to read and then I do some other thing that improves the code. Well, let's see first in the old version, I have a nested layer. So I have an if statement at the outside and we have two inner if statements. So in this code set, we have actually three different if statements. And now in the newer version, I only have one if statement and I have unnested, so to say, the code. And now I can just read it like this. If the coin is hats and the number is even print this. If the coin is hats and the number is not even do this. If the coin is not hats and the number is even do this. So this now almost reads like English, right? So now I don't really have to look at the body here at what the code is actually executed in the four branches. I can basically look a little bit before the if statement and I see the temporary variables here that I set and they are self-explanatory. And then if I read the conditions, then they are also very easy to read. And imagine that the code block in here is not only one line of code, but maybe let's say 100. So you wouldn't get an overview here. So how would you try to understand this code? It would be hard to do. So now with the self-explanatory variable names and also the easier logic, the unnested if logic here, I think this code is at least easier to read. I would still have to give you the background of AB testing because I think this is obviously not apparent from the problem, but at least when we read this code, we have an easier time reading it. And of course, the code is still correct. And now that was the if statement and now comes the if expression. So in the first chapter, I emphasize the difference between the conceptual difference between an expression and a statement. An expression was something that evaluates in memory, but does not have any permanent side effects. And a statement usually has permanent side effects. So what is the if expression? Let's look at an example. I give you a function y is f of x. And whenever x is smaller than zero, my y is supposed to be zero as well. And if x is greater than zero, then y is supposed to be x. So what does this function look like? But that's rather easy. So if I draw my x, y coordinate system, and I want to draw in a function, what does the function look like? It's a line that is close to zero. It's actually zero coming from the left. And from the origin, we have the diagonal here. That's the function that we are modeling. And so you may wonder what is an application of such a function? Well, in business, or in finance, this is an example of an option. So this is how we would price an option. Functions like this are used for pricing options. So there is actually quite a good business application behind such functions. How could we express this function in code? Well, with what we've learned so far, we could say, let's say x is three. I can write an if statement. Just as we saw, I compare is x smaller than or equal to zero. Then I set y to zero, otherwise I set y to x, and then I give out y. And the answer is, of course, three here. And if I go ahead and make it a negative three, I get back a zero. So the function or the code cell does what we wanted to do, but it looks rather lengthy, right? It should be, we have the feeling that somehow we should be able to make this in less lines of code. And we can using the if expression. So the if expression works like this. Given an x, we say we have the expression here, and the expression is zero if this condition is true, otherwise else we get x. So in other words, we say if x is smaller than or equal to zero, then the expression will evaluate to zero, otherwise it will evaluate to a y. So if I execute this, I get back at the same result. And the nice thing about this is I actually don't need the y variable here. So I can actually delete the y variable, and I can execute this, and I get back three also. And the reason why I get three here is because this is an expression. So if I write, let's say negative three here, it would be, or negative 33, it would be zero. Okay, so let's keep the y in. So now just to make a point here, the if expression is of course nicer in this regard. Whenever we model like, let's say a binary choice or something like this, however, in this particular situation, there's even a better way to do it, and we just use the built in max function. So this also gives us the same result. But the point of this section is to introduce the if expression. And that means we use the if in a totally different context. Okay, let's get to something else. And something else is the try statement. So far we have seen how we can execute code if a certain condition is true and other code if a condition is not true. We've also seen so far many examples of code failing. So code raising an exception to us. And we have a natural question that arises here. Is it possible whenever an error or an exception is thrown by the computer, by Python, can we actually catch it? In other words, can we react to the occurrence of an exception? So basically an exception occurs. If an exception occurs, we do one thing. And if an exception does not occur, we do another thing. And this is exactly what the try statement is for. So the try statement helps us to execute code only in the case when something goes wrong. I give you an example. So assume we have some user input. So that means we built software and some customer, some user enters data. So we have to expect that our user will enter data in an invalid way that will result in an error possibly. For example, let's assume this is just an experiment here. The customer will enter a number between zero and five, randomly. And then whatever the number is that the user entered, we will use it to divide one by this number. And then of course we have a one in six chance. This calculation will not work because divide by zero error. So let's try. So this time worked, it worked, and now it doesn't work. So that's a code to sell that sometimes works and sometimes doesn't. And this is typical, right? So most of the time our customer knows how we want him or her to work with our software and does so without an error. And sometimes the person using our software will just mess up. So how do we react to such a situation? Well, let's write a so-called try statement. So this is the same code. Just that the one over user input, so the division here is now put inside the try statement. And actually we could leave away the print here as well. So sometimes we don't see anything and when we don't see anything, everything worked. And sometimes we see that the message, something went wrong is printed. And what that means is in this situation, the number zero was drawn and one was divided by zero. And then we see that the except clause kind of is the thing that happens or that takes over when an error occurs. So maybe we should have written it like this. Result is equal to this. And then let's just print to result is like this. So now I always get a nice message. So the result is five. And then sometimes every once in a while I get an error message. Okay, so now we learn that whenever we put code within the try branch of a try statement, then this code, if it raises an error, we can catch the error and we do so with the accept clause. Now this is generic here, right? Try accept basically means try this code and whenever anything goes wrong, do this. This is what try and accept here means. That's not a best practice. The best practice is a little bit different. So the best practice goes like this. We have our try clause. We put in the statement or the code that may go wrong that may result in an error. And then in the accept clause, we mention precisely which error we are expecting. So here we will only look for the zero division error and if any other error occurs, this will not be caught here. And why is this a best practice? Well, quite simple. We don't know that many, many things that can go wrong as a computer program runs and we only want to handle exceptions that we can predict so that we can actually expect to happen. And in this context, we can only expect a divide by zero error. So if anything else goes wrong in this code block, I don't want to handle it. I rather want the computer to really run into an exception so that I see in my error logs later that really something went wrong. So this does the same thing. And again, this code is just a little bit nicer because I put the word zero division error here and I'm looking for exactly this type of error. And now the try statement has in total four different clauses that are possible. So the try statement, of course, has a try clause because that's the code that we are running, the code that we don't know if it works. Then we have accept clauses and here I have exactly one accept clause but the rule is I can have as many accept clauses as I want and then for every accept clause I have to specify which errors I'm looking for. So this clause, the accept clause here is printed or is executed whenever the zero division error occurs. And in this case, we just print a nice error message. And then there is an else clause. So what does the else clause do? Well, the else clause is where we put code that will only be executed if the try clause does not result in an error. In other words, whenever the code here in the try branch raises an error, we end up in the accept clause and whenever the code in the try clause does not raise an error, we end up in the else clause. So this is the success case here. And then there is a finally branch and the finally branch is basically a branch that is always executed. So basically what this means is we execute this here. If it goes wrong, we also execute, we stop execution here because there was an error. We continue execution here and then after we are done here, we continue execution here. And if everything goes correct, in here in this clause here, then we continue execution in the else clause. And then after we are done in the else clause, we go to the finally clause. So what's the idea of the finally clause? Well, maybe you have in your code, you need to open a connection to a database and when something goes wrong or independent of something going wrong, you want to let's say close the connection to a database and closing the database connection, the code for doing so, you would put here in the finally clause. So that no matter what happens, the connection to the database always closed. The same holds true for let's say writing files to disk. So whenever you have your data in memory, then you don't want the computer program to just crash, but you want when an error occurs, the intermediate results of your calculation, maybe you want to store them to disk or something. And this is typical code that you would put in a finally branch. Okay, so let's execute this code set a couple of times and we will see that either an error occurs or no error occurs and I am always printed will always occur here. So this is how we deal with errors. So this is how we handle errors in code. And I put it in this chapter because it is conceptually similar to an if statement. In an if statement, we handle conditions and here we handle the condition of, let's say an error occurring or not. So in a way it's conceptually similar. Okay, that was chapter three and next chapter we will talk about iteration.