Since if statements are so useful, they can be a little overwhelming when you get started.
This lesson gives some examples of patterns that can help guide your thinking about using if statements in different ways.
These patterns are not officially part of the Python language.
They are merely templates that we have created based on common patterns we see in code.
They are not exhaustive since there are many ways to leverage and twist conditionals.
But if you treat them as starting points, you may find that they inspire you in different directions.
if __A__ and __B__:
# ...
# Is the same as...
if __A__:
if __B__:
# ...
An if statement nested inside of an if statement is equivalent to having both conditions combined with an and statement.
The advantage of an and expression is usually brevity since the code will only take up a single line without an extra indentation.
The advantage of the nested if statement, however, is a little more nuanced.
Statements can be added before or after the inner if statement, allowing more statements for the first case but not necessarily for the second.
You can also specify additional cases’ behavior by adding in elif and else statements to the inner if statements if you need to narrow the behavior further in the case where the first condition is true and the second condition is false.
if weather.lower() == "sunny" and temperature > 70:
print("Warm!")
if weather.lower() == "sunny" and temperature < 30:
print("Cold!")
# Is the same as...
if weather.lower() == "sunny":
if temperature > 70:
print("Warm!")
if temperature < 30:
print("Cold!")
As a concrete example, observe this code. The first version has to have redundant versions of the weather.lower() check. The second version has an extra line of code because of the second if statement. Just because a program takes fewer lines of code does not mean that that solution is more readable; with less redundant logic, the second block of code is less likely to have typos once you get used to reading nested if statements.
if ___:
# Potentially unsafe operation
There are operations and functions which are valid syntactically but will cause an error in practice. The Defensive Guard pattern is just the concept of “protecting” those operations from occurring by first checking that the data involved is valid. The pattern is simple, just literally an if statement. But the concept has nuance.
# Guard against division by zero
if time > 0:
speed = distance / time
# Guard against bad conversion
if user_input.isdigit():
age = int(user_input)
A classic example is division by zero, which causes a ZeroDivisionError. Using an if statement, you can make sure that a variable such as time is greater than zero and therefore safe to use for the denominator.
As another example, you can use the isdigit method of strings to make sure that the string is all numbers before you try to convert it to a float or an int.
# Truthiness to test if there are ANY characters
if user_input:
last_character = user_input[-1]
first_character = user_input[0]
# Check length to make sure that there are at least three characters
if len(user_input) >= 3:
third_character = user_input[2]
One other common check is testing the length of a string before you index a specific character. If you want the first or last character, then you must determine that the string is non-empty. This can be easily done using truthiness. However, if you want a specific inner character, you must check that the string is at least that long.
# Early Return
def ___(___):
if ___:
return ___
# do something ...
return ___
The Early Return pattern is like the Defensive Guard pattern, with an if statement ending a branch inside of a function before an error (or other undesirable operation) can occur.
def get_punctuation(message: str) -> str:
if not message:
return "empty string"
last_character = message[-1]
if last_character == "?":
return "question"
elif last_character == ".":
return "statement"
return "other"
In the example shown here, we check if the message parameter does not have a truthy value.
For a string, any non-empty string will be considered truthy.
In this case, then, indexing is only attempted if the message is not empty.
This is very important, since indexing the last character of the string will cause an IndexError if the empty string was indexed.
The early return prevents the mistake from occurring, allowing us to safely use the logic in the rest of the function to determine what kind of punctuation symbol the last character of the string holds.
def ___(___):
if ___:
return ___
elif ___:
return ___
# ...
else:
return ___
Parsing code with multiple returns can be a little tricky.
What gets returned? The key thing to remember is that a function can only return exactly once.
That will happen regardless of whether you have a return statement.
But if a return statement is encountered, the function is over, and whatever value is specified in the return statement is what is brought back to the original call site.
That’s why having more than one return statement only makes sense when you have if statements.
The if statements can disrupt the control flow of the function, branching off different paths for each return statement.
For each call to the function, only one path needs to be taken, with only one return statement being activated.
def convert_ordinal(number: int) -> str:
if number == 1:
return "first"
elif number == 2:
return "second"
elif number == 3:
return "third"
else:
return "other"
In this example, the convert_ordinal function consumes an integer and produces a string representation of the ordinal number, such as “first”, “second”, or “third”. The value stored in number is matched against each of the possibilities in turn, but only one can be true since we are using a mutually exclusive chain of if/elif/else statements.
Ultimately, if none of the expressions evaluate to True, then the else block is entered and the value "other" is returned.
def ___(___):
if ___:
result = ___
elif ___:
result = ___
# ...
else:
result = ___
return result
A related pattern is the Build-Up-Return pattern.
How is this different from the Multiple Return Spots pattern? Functionally, they are equivalent.
The value being returned is meant to be the same in either pattern.
The difference comes in the number of return statements, and the exact path that the control flow takes through the program.
Some folks try to avoid having multiple return spots in their program since it is a common source of mistakes in beginner programs.
For those struggling with control flow, it can make a lot of sense to avoid trying to mix the confusing branching of if statements with the abrupt finality of return statements.
The Build-Up-Return Pattern can help you isolate the return to a single spot, making it easier to think about.
def convert_ordinal(number: int) -> str:
if number == 1:
result = "first"
elif number == 2:
result = "second"
elif number == 3:
result = "third"
else:
result = "other"
return result
In the previous convert_ordinal pattern, we had return statements inside of the if statements.
But now, there is only one return after all the conditionals.
Instead, assignment statements are used.
The result is still the same, though.
if ___:
if ___:
value = __special__
else:
value = __default__
else:
value = __default__
# Becomes
value = __default__
if ___:
if ___:
value = __special__
The else statement is a powerful way to specify default behavior after if and elif statements.
But, when nesting these statements, the else branch is not always conveniently available.
The nested statements represent specific, narrow cases involving multiple conditions.
To provide a default situation with else statements, every single level of nested if would need to have an else statement, sometimes leading to redundant code.
However, the Define-and-Refine pattern allows you to specify an initial value for a variable and then only specify the branches to change that value.
# Messy!
hour = int(input("What hour is it?"))
day = input("What day is it?")
if day == "Saturday" or day == "Sunday":
if hour < 12:
can_sleep_in = True
else:
can_sleep_in = False
else:
can_sleep_in = False
print("Can sleep in?", can_sleep_in)
In this code example, the program is attempting to determine whether the user can sleep in. It must not only be a weekday, but it must also be before noon. However, with the way the code is currently structured, we must provide an else body for each of the conditionals. This is inconvenient since the action to be taken (assigning False to can_sleep_in) is the same along both of those paths. The Define-and-Refine Pattern suggests a solution to clean up this code.
# Cleaner
hour = int(input("What hour is it?"))
day = input("What day is it?")
can_sleep_in = False
if day == "Saturday" or day == "Sunday":
if hour < 12:
can_sleep_in = True
print("Can sleep in?", can_sleep_in)
In the new version, we first initialize the can_sleep_in variable before we structure the if logic chain. That way, we no longer require any else paths at all. This only works because the behavior was the same along all those paths, though. We couldn’t have made this change if either path had different behavior. Granted, both of these programs also ignore the many good people who sleep in past noon, but let us leave them aside for the moment.
Sometimes there is readability value in having an explicit else case, so that future developers will understand that you meant two cases to be distinct from each other. With every one of these patterns, you must think critically about how you can make the code as readable as possible, and whether the pattern improves the code or not.
There are many ways to use if statements to improve readability and control your code’s flow:
if pattern observes that nesting if statements is functionally equivalent to chaining together and operators.if statement to avoid errors.return statements by using assignment statements instead.else statements for a complex conditional.