:tocdepth: 2 .. _cond_if: .. note to self order of operations in math -> operator precedence in programming ********************************************* Conditionals, I: **if**, **else** and more ********************************************* .. contents:: :local: .. highlight:: python Conditionals and program flow ============================================ By default, the Python interpreter will start at the top of a script file and move downwards, systematically executing each successive line exactly once until it reaches the end of the file. This is what would happen if our script only contained the features we have discussed so far: math functions, operator expressions and assignment of variables. However, we might want to set up our code to calculate some values, and then execute certain lines *only* in the case of a particular outcome. For example, if a number is a perfect square, print it with text that has an exclamation point. Or we could check a result, and if it were positive do one set of commands (say, double it and add three), but if it were negative do a different set (say, cube it and subtract five). That is, we evaluate a result and *choose* what to do next. And in this case, it is possible that not every line of code will be executed: some might be skipped over (e.g., if we found no perfect squares or no odd numbers, in the examples above). We are affecting and controlling the *flow* of the program in a non-trivial---and hopefully useful!---way. This kind of decision making is called **branching**, as if one were climbing a tree and deciding which of two branches to follow. Branching command flow can be done using a **conditional statement**: we establish a condition, and if it evaluates to true, Python executes commands A, B, C, \.\.\.; otherwise, it executes X, Y, Z, \.\.\. This is another fundamental item in our toolbox of algorithmic programming, and there is a lot of flexibility which how conditions are structured. The most common way to start a conditional in an English sentence or math text is using the word **if**. This is also the keyword to start one in Python. Therefore, conditionals are also known as **if statements**. There is a family of them, with related meaning and syntax, but differing in the number of branching points. We explore each briefly in turn. .. note:: While a spoken language may have many synonyms like "in the case of" or "in the event that", within the programs there will only be ``if``: what it lacks in imagination, it makes up for with clarity. Here and in general, it is useful to be able to map back and forth between a conceptual/colloquial expressions and a strict program syntax. Part of the art of programming is often to rephrase a question or element of a problem in a helpful way. **If** statements ============================================ In words, a basic conditional statement can be expressed as, "If some condition is met, then do these specific commands." This conditional phrasing can be mapped into Python syntax as:: if : And, without adding any further text, this also implies that when that condition is *not* met, then those specific commands will *not* be done. (Other members of the if-condition family deal with these aspects differently, discussed below). There are several important things to note about the conditional's syntax (particularly since we will see some similar features of this syntax in several other Python structures): * ``if`` is a keyword in Python, and it is recognized as the beginning of the conditional. * The ``some condition`` is a **statement** (also known as a "predicate") that immediately follows the keyword ``if``. It is an expression that Python will interpret as a Boolean value: whatever form it takes it is evaluated as either ``True`` or ``False``. Note that the statement may be complicated and contain several pieces (numbers, relational operators, comparisons, etc.), but it is evaluated as a single ``True`` or ``False``. * A colon ``:`` follows the statement, and it is necessary syntax to define the end of the statement. If you want to think about mapping this code to a spoken sentence, then in this context you could think of this as the "then". * The line following the ``if`` statement *must* be **indented** relative to ``if``; **indentation** is the set of spaces at the start of the line. The Python standard for indentation is 4 spaces. * The ```` to be done may occupy one or more lines. Each line must be indented the same amount. That is how Python recognizes what is "inside" this if condition branch (i.e., all to be done if the statement evaluates to ``True``). * The Python interpreter knows that it has reached the end of the list of the ```` to do when it either: 1. Reaches a line that is *not* indented relative to ``if``, #. Reaches the end of file (**EOF**), or the end of the Jupyter-notebook cell. So, for example, assume we have some int or float ``x`` that is defined. Then the conditional, "If x is negative, calculate x-cubed and print the result," could be translated as: .. code-block:: Python :linenos: if x < 0 : xcub = x**3 print("x-cubed is:", xcub) print("I am printing from *outside* the condition!") Line 1 starts the conditional and provides the statement to be evaluated as True or False. After Line 1's colon, the Python interpreter starts looking immediately for indented lines, and it finds two of them: Lines 2-3 will be executed *only if* *x* is less than zero. Line 4 is a bit extra: it is not indented, so it is one way to signal the end of the conditional branch. Python recognized it as being *outside* the branch, and will be executed *regardless* of the value of ``x``. We will see 2 lines printed if ``x`` is less than zero, otherwise just one line. Here are some other examples of valid ``if`` conditions (again, assuming each primary variable has been properly a assigned value already): * Chained comparison, Example 1:: if 100 <= x < 1000 : print("Medium-sized x", x) * Arbitrarily large expressions can be tested, Example 2:: if type(my_value) == int and my_value > 250 : print("Oh good, this value is both an int") print("... and it is pretty large:", my_value) * Check a property of a variable, and possibly re-define it, Example 3:: if not(type(y) == int) : print("Oh darn, a non-integer value:", y) y = int(y) print("I have now converted that variable value to be an int:", y) print("Whether you wanted it or not, you have an int now!") .. container:: qpractice **Q:** How many ``print()`` function calls are made for each of Ex. 1, 2 and 3 above if the conditional statement evaluates to ``True``? How many, if it evaluates to ``False``? .. hidden-code-block:: python :label: + show/hide response # for True statements: # Ex 1 : 1 print() # Ex 2 : 2 print() # Ex 3 : 3 print() # for False statements: # Ex 1 : 0 print() # Ex 2 : 0 print() # Ex 3 : 1 print() The following are *invalid* if-conditions, because they have incorrect syntax in various ways: * Bad "if" syntax, Example 4: .. code-block:: Python :linenos: if 100 <= x and x < 1000 print("Medium-sized x", x) \.\.\. which produces: .. code-block:: Python :linenos: File "", line 1 if 100 <= x and x < 1000 ^ SyntaxError: invalid syntax Why is this a ``SyntaxError``? There is a missing colon after the predicate statement. Note that the interpreter has picked out a problem in "line 1", and see where it is pointing an arrow ``^``. * Bad "if" syntax, Example 5: .. code-block:: Python :linenos: if 100 <= x and x < 1000 : print("Medium-sized x", x) \.\.\. which produces: .. code-block:: Python :linenos: File "", line 2 print("Medium-sized x", x) ^ IndentationError: expected an indented block Why is this an ``IndentationError``? There is missing indentation after the colon. Note that the interpreter picks out a problem in "line 2", and its arrow ``^`` is pointing at the command ``print``, which it expects to be indented (since the line immediately follows the ``:``). **Mini-summary:** This ``if`` conditional allows us to program the following kinds of expressions, "If X is true, do Y," where X can be as complicated a statement as desired, and Y can be a list of one or more commands. Python demands certain syntax to be able to interpret this, including the colon ``:`` at the end of the predicate statement X and indentation of each of the conditional commands in Y. .. _indentation_note: Notes about indentation ------------------------ **Conceptual comment** As noted above, Python syntax uses **indentation** (whitespace at the beginning of a line) to denote what is *inside* the if condition and where it ends. This is actually a pretty nice syntax to have, because it provides a strong visual cue for both the start and duration of the conditional. Even in programming languages that do no *require* indentation for conditions, many people tend to still indent anyways, to clearly identify to themselves and other programmers where the commands inside the condition are. We can think about this conceptually like the form an outline is written: a section is introduced by a heading, and then items that belong in that section are indented. It is visually apparent when a new heading comes along, because it is unindented back to the level of the other headings (or the list ends). For example, we could compile a list of operators for different Boolean expressions: Logical operators: | intersection, ``and`` | union, ``or`` | negation, ``not`` Membership operators: | containment, ``in`` | absence, ``not in`` Comparison operators: | equality, ``==`` | inequality, ``!=`` | greater than, less than, ``>, <`` | greater than or equals, less than or equals, ``>=, <=`` As one reads down the list, it is quickly apparent where the heading items are and which items are "inside" each heading's section. Parallel headings have the parallel indentation---that is, indentation of the same size, which is zero here. We can have one, two, a hundred or more under each heading. Keeping each item indented an equal amount is useful for visual recognition (and in Python, it is required). We put this conceptual note here because we will use indentation *a lot* in Python programming. Below, we will use it with more members of the conditional family. Later, we will use the syntax in writing loops and functions. Much later, we will use it in nesting structures (containing conditions inside conditions, or inside loops, etc.), and indentation will still be used to organize these levels. **Implementation suggestion** When writing code, indentation may be made up of spaces and/or TABs. Tabs are convenient to indent with a single keystroke, but they are also unstable, because different editors and/or computers will represent them with different lengths. This can break Python code! **Therefore, for portability to different computers and text editors, using only spaces is highly preferred.** In most text editors, you can set TABs to be replaced by spaces. So, you can still conveniently use the TAB key while also instantly removing the instability. The typical indent size of 4 spaces balances readability with space considerations, but you can choose a different indentation size *as long as you are consistent with it in a code.* **If/else** statements ============================================ The above "if" conditional only provides commands for when a statement is True; it only offers one branch. The related **if/else** conditional adds the ability to provide alternative commands if the predicate statement is False, adding a second branch. The "if/else" conditional statement can be generally expressed as, "If some condition is met, then do *these* specific commands; otherwise, do these *other* specific commands." This translates into Python as:: if : else: Note that all of the above rules for the ``if`` branch apply, such as the colon and indentation syntax. Now, we have a new branch that starts with the keyword ``else`` (the Python translation of "otherwise"). There is no statement following ``else``, simply a colon ``:``. Note that ``else`` is not indented relative to ``if``: this makes it visually apparent that they are parallel branches (e.g., like in an "outline", :ref:`described above `), and it also closes the ``if`` branch's ```` list. The same rules apply for the ``else`` branch's ```` as for the ``if`` branch's ````: there may be one or more, and each must be indented from ``else`` by a constant amount. Python again knows the end of the ``else`` list of commands when either the next line is not indented or the file ends. Here are some examples: #. An example of categorizing a random integer: .. code-block:: python :linenos: x = random.randint(0, 99) # get a random int in interval [0, 99] if x < 50 : print("This number is small:", x) else: print("This number is big:", x) print("Done checking the number") #. We convert one of the "if" examples above to an "if/else" case: .. code-block:: python :linenos: y = if type(y) == int : print("Oh good, an integer value:", y) else: print("Oh darn, a non-integer value:", y) y = int(y) print("Now I have *made* it an integer value:", y) print("Whether you wanted it or not, you have an int now!") The cases of whether ``y`` is or is not an int are made explicit here. The final ``print`` function is outside of the conditional (which is signaled by its lack of indentation relative to the ``if``\/``else`` keywords). From a *flow* point of view, Lines 1 and 2 are always executed. If ``y`` *is* an int, then Line 3 will executed, and then Python skips the entire other branch, jumping to Line 8; otherwise, Python jumps from Line 2 to Line 4, and will execute lines 5-7 within this branch, and then exit the branch and continue to Line 8. Again, Python's use of indentation provides a nice, human-readable visual cue to check what is inside of a conditional branch. We can immediately tell that there is one command following the ``if`` branch, and three commands following the ``else`` branch. We also know that one more command will be executed after the condition. #. Let's translate the following into a block of Python code: *"Let x be a number. If it has 9 as a factor, print how many times 9 goes into it; otherwise, just print the original number."* First, we have more clearly state our predicate statement, translating it into a mathematical Boolean statement. What is a mathematical expression we can use to check if one number is a factor of another? (Hint: could we test something about the remainder of division? We do have an operator to calculate that \.\.\.) OK, so we could rewrite the problem in a more mathematical way: *"Let x be a number. If the remainder of x/9 is zero, then ..., else ..."* This is now something that can be translated into Python code: .. code-block:: python :linenos: x = random.randint(0, 99) # an arbitrary way to get a value to check if x % 9 == 0 : print("How many times does 9 go into x?", x//9) else: print("Sadly, this number does not have 9 as a factor:", x) **Framing a question or scenario properly is one of the most important aspects of programming.** It occurs before one starts typing code. Breaking a problem down into smaller pieces and formulating each one clearly is vital, and it takes practice. That is precisely why why we are practicing here! The following are some *invalid* attempts to use if/else, based on examples presented above: * Bad "if/else" syntax, Example 1: .. code-block:: Python :linenos: y = 25.0 if type(y) == int : print("Oh good, an integer value:", y) else: print("Oh darn, a non-integer value:", y) y = int(y) print("Now I have *made* it an integer value:", y) print("Whether you wanted it or not, you have an int now!") \.\.\. which produces: .. code-block:: Python :linenos: File "", line 4 else: ^ SyntaxError: invalid syntax Why is this a ``SyntaxError``? For ``else`` to be valid, it must be parallel to an ``if``. As we said above, parallel branches must have the same indentation level. That is not the case here, and so Python is rightfully unhappy. To fix this, one could unindent ``else``, as well as the three lines following it (they should just have a single level of indentation from their starting keyword). * Bad "if/else" syntax, Example 2: .. code-block:: Python :linenos: y = 25.0 if type(y) == int : print("Oh good, an integer value:", y) else: print("Oh darn, a non-integer value:", y) y = int(y) print("Now I have *made* it an integer value:", y) print("Whether you wanted it or not, you have an int now!") \.\.\. which produces: .. code-block:: Python :linenos: File "", line 7 print("Now I have *made* it an integer value:", y) ^ IndentationError: unexpected indent Why is this an ``IndentationError``? And why is Python complaining about Line 7? Looking at the original example, we can see that the badness (or at least alteration) comes from Line 6 *not* being indented. Well, let's think about the interpeter's flow: It interprets Line 1 fine; then Line 2 fine; and since the predicate statement evaluates to ``False``, the interpreter jumps to Line 4 and enters the ``else`` branch, starting with Line 5, which is indented. Then, since Line 6 is *not* indented relative to the ``else``, the interpreter believes that the ``else`` branch is complete, and that it is outside the conditional. Therefore, when the interpreter sees Line 7 indented, it is surprised and unhappy, because there is no reason for it to be indented. Hence, our mis-typing in Line 6 caused an error for the interpreter in Line 7. This is an example of a subtle kind of error that can occur in programming, and which can be difficult to debug if we are not careful. This provides a good lesson, too, about Python's debugging. As we have seen above, it is quite useful and can often point to the actual location of the coding error. However, it cannot read our minds, and sometimes an earlier (or "upstream) mistake in the code is actually the root cause of an error. Also, if Line 7 weren't included or had also been mistakenly unindented, the interpreter might not even have found any error here---yikes! This is why we must always test our code as we go along, to make sure subtle errors don't creep in. * Bad "if/else" typing (but allowed syntax), Example 3: .. code-block:: Python :linenos: y = 25.0 if type(y) == int : print("Oh good, an integer value:", y) else: print("Oh darn, a non-integer value:", y) y = int(y) print("Now I have *made* it an integer value:", y) print("Whether you wanted it or not, you have an int now!") \.\.\. which produces: \.\.\. no error! However, relative to our code above, we know that this is a mistake. As typed, the string "Now I have *made* \.\.\." would *always* be printed, which is not correct. In terms of *our* meaning, this line should be indented, but there is no Python syntax error for the interpreter to find and report, so we see no warning. Again, we programmers must be careful because things like this can be tricky to debug. **In summary**, the ``if``\/``else`` conditional allows us to program the following kinds of expressions, "If X is true, do Y; otherwise (=else) do Z." Each of the Y and Z will be one or more commands, and the same syntax rules apply for each---namely, each set is constantly indented within their own section. The ``else`` line ends in a colon, just as the ``if`` line does. The "else" is logically parallel to the "if", and in programming syntax this is mirrored by the fact that ``else`` has no indentation relative to its partnered ``if``. **If/elif/else** statements ============================================ Finally, the branching conditional can be generalized to test more than one (separate) predicate statements, using one or more ``else if`` lines. This allows there to be any number of branches. The **if/elif/else** conditional can be expressed (trying give a sense of the wide generality) as, "If some condition is met, then do *these* specific commands; otherwise, if this *other* condition is met, do these *other* specific commands; or otherwise, if this still *other* condition is met, do these *other* specific commands; \.\.\. and finally, if *none* of those conditions is met, do these *last* specific commands." This conditional is translated into Python as:: if : elif : elif : else: There can be any number of ``elif``\ s, and all the rules that we described for the ``if`` line apply to the ``elif`` line. Note that there doesn't even need to be a final ``else``, if one does not want a "fallback" case of doing something when no tested predicate is true. Basically, this structure generalizes all of the conditionals discussed above. **Note: at most only one branch in a conditional can be executed. Even though multiple predicates could evaluate to True, Python will go through in order, find the first one that is True and execute its branch of commands, and then jump to the end of the conditional immediately.** Sometimes, the order of statements in the if/elif sequence doesn't really matter. This is only the case if the predicate statements are non-overlapping (or "mutually orthogonal"). For example, whether we first test if the remainder is 1 or 0 would not affect this outcome: .. code-block:: python :linenos: x = random.randint(0, 99) # an arbitrary way to get a value to check if x % 3 == 0 : print(x, "is perfectly divisible by 3.") elif x % 3 == 1 : print("We get a remainder of *1* when dividing", x, "by 3.") else: print("By process of elimination we know the following:") print("... that we get a remainder of *2* when dividing", x, "by 3.") However, there are many applications where that is *not* the case, and the order of the conditions is critical, because only the branch of the first true one will be executed. Consider the logic of the following construction: .. code-block:: python :linenos: dist_km = int(1000*random.random()) if dist_km > 500 : print("Far distance. Looks like I'm flyin'!") elif dist_km > 200 : print("Well, I will check a train schedule!") elif dist_km > 49 : print("Not too bad, I guess that's a bus ride.") elif dist_km > 5 : print("Maybe my friend can drive.") elif dist_km > 2 : print("Time for a jog! I wanted some exercise, anyways.") else: print("OK, I will walk.") print("... just to travel", dist_km, "km.") What is the flow when ``dist_km`` is 300, and each of those ``elif`` statements is actually True? Well, the statement in Line 2 evaluates to False, so Python skips its branch and jumps to the next parallel conditional keyword, ``elif`` in Line 4. The statement there is True, so Python executes its branch (Line 5). It then skips *all* remaining branches, jumping to Line 14. But if we swapped the branches that start in Lines 4 and 10, would lead to different outcomes (and ones that make less sense, based on the logic of traveling). *In summary:* * Conditionals allow us to control the code flow, so that subsets of commands are executed only under certain conditions. * Each branch of a conditional can contain one or more commands. * With the addition of ``else`` and ``elif``, ``if`` conditions are quite flexible for specifying a range of conditions and alternatives. * When we have ``elif`` statements, we should make sure that they are ordered in way that makes sense, particularly when tested criteria overlap. * At most, only one branch of any conditional will be executed. * Indentation is key to defining the structure of a conditional. Indentation determines: * what commands are in a given branch; * where the end of the conditional is * which if/elif/else branches are in parallel (esp. when we see later how conditionals can be nested inside each other). .. note to self: + somewhere also mention that one can do things like: if x-20 : because the argument will get evaluated like a boolean; nonzero -> True zero -> False sometimes 'True' means error, because we write things in the present notation so that having 0 means good, and anything else is a prob | Practice ============================================ #. For each of the following, ponder first, then check in the terminal. a. How many print statements get evaluated here? .. code-block:: Python x = 3 if x > 25: print("This number is big!") print("Where did you find it?") print(x) #. How many print statements get evaluated here? .. code-block:: Python x = 3 if x > 25: print("This number is big!") elif x < 10: print("This number is small!") print(x) #. Fix the following:: x = 3 if x > 25 print("This number is big!") #. Fix the following:: x = 3 if x > 25 print("This number is big!") else: print("This number is small!") #. Can you have an ``if`` without an ``else``? Can you have an ``else`` without an ``if``? #. Write an ``if`` condition to check if a variable called ``test1`` is greater than 5. If it is, print "Congratulations". #. Write an ``if`` condition to check if a variable called ``test2`` is odd. If it is, print "I found an odd number"; otherwise, print "I found an even number". #. Write an ``if`` condition to check if a variable called ``test3`` is *not* an int. If it is not, then print "Fancy number". Otherwise, do nothing. #. Write an ``if`` condition to check if a variable called ``test4`` is in the interval ``[-3, 3]``. If it is, print "Small number!". If it outside that range but in the range ``[-10, 10]``, print "Medium number!"; and if it is outside of that range, print "Big number!" #. Write an ``if`` condition to check if a variable called ``test5`` is complex or a float. If it is complex, print "I found a complex number"; if it is a float, print "I found a float"; otherwise, print "Oh well". #. Write an ``if`` condition to check if a variable called ``test6`` is a negative integer. If it *is*, multiply it by ``-3`` and see if the result is a multiple of 9; if *that* is, then print "hello, my friend". In any other case, print "Oh, no, not you". #. Make a conditional that checks the ``type`` of a variable named ``test7`` for each of the following that are familiar to us: int, float, str, bool and complex; if the type is none of the above, then just print "this type is unfamiliar!", along with the type. In each case case of a familiar type, print the type, and if it is: int, print the square of it; float, the cube of it; bool, the opposite of it; complex, the magnitude of it; and string, 4 copies of it appended together. #. Write a program that prompts for two numbers, ``a`` and ``b``. If the ``b`` is not zero, print the quotient ``a/b``; otherwise, print an error message that division by zero is not allowed. #. Write a program that prompts for a string ``s``. If the length of the string ``s`` is odd, then print it with ``'ing'`` appended to its end. If the string length is even and non-null, print the first (i.e. :math:`[0]`\ th) character and the string's length. If the string is null, print "Empty!". (NB: *think about the simplest ordering for presenting these conditions for most efficient/simplest code*). #. Write a program that prompts for 3 numbers to be saved as ``a``, ``b`` and ``c``, and prints them in ascending order. #. Write a program that prompts for a ``score`` between 0 and 100 (inclusively). If the value of ``score`` is out of range, print an error message. If ``score`` is in the appropriate range, check whether it is high (``score >= 80``) or medium (``50 <= score < 80``) or failing (``score < 50``). #. Write a program that prompts the user for 2 integers ``a`` and ``b``, and then solves the quadratic equation :math:`x^2 + ax + b = 0.`