14  Conditionals

“The best thing about a boolean is even if you are wrong, you are only off by a bit.” — Anonymous.

In programming, conditionals are used to make decisions based on certain conditions. They allow you to control the flow of your program by executing different blocks of code based on whether a condition is True or False. The “off by a bit” reference in the quote above is a nod to the binary nature of boolean values, which can only be True (1) or False (0).

Conditional statements allow control flow in a program, enabling it to perform different actions or execute specific blocks of code based on the evaluation of a condition.

14.1 Rule-Based Decision Making

In real-world scenarios, decision-making often involves a series of rules or conditions that guide the process. For example, consider a grading system based on the score of a student. The decision tree in Figure 14.1 shows how a student’s score can be used to determine their grade.

Notice that the decision tree branches based on the score of the student. If the score is greater than or equal to 90 and less than or equal to 100, the student receives an "A". If the score is between 80 and 89, the student receives a "B", and so on. The decision tree represents a rule-based system that maps input (the student’s score) to an output (the grade).

Rule-based systems is a common approach in programming to represent domain-specific knowledge or logic. The rules are typically expressed as conditional statements that guide the program’s behavior. It is often referred to as the first level of artificial intelligence (AI) and is used in expert systems, chatbots, and other applications.

graph TD
    A((Start)) --> IN[/Score/]
    IN --> B{"Is<br>90 <= Score <= 100?"}
    B -- Yes --> C[/A/]
    B -- No --> D{"Is<br>Score >= 80?"}
    D -- Yes --> E[/B/]
    D -- No --> F{"Is<br>Score >= 70?"}
    F -- Yes --> G[/C/]
    F -- No --> H[/D/]
    C --> I((End))
    E --> I
    G --> I
    H --> I
Figure 14.1: Decision tree for grading based on a student’s score.

14.2 Conditional Statements in Python

In Python, conditionals are expressed using:

  • if statements
  • ifelse statements
  • ifelifelse statements
  • match statements (introduced in Python 3.10)
Statements vs. Expressions

Statements are pieces of code that are executed whereas expressions are pieces of code that are evaluated to produce a value. Therefore, statements will perform an action (e.g., assigning a value to a variable, checking a condition, looping, or defining a function) and will often comprise multiple expressions.

14.3 if Statements

An if statement is used to conditionally execute a block of code. The code block is executed only if the condition specified in the if statement is True. The syntax of an if statement is as follows:

if condition:
    # Code block to execute if condition is True
# Code block outside the if statement

The code block inside the if statement is indented to indicate that it is part of the if block. If the condition is True, the code block is executed; otherwise, it is skipped.

For example:

x = 6

# Check if x is greater than 5
if x > 5:
    print("x is greater than 5")

In this example, the code inside the if block will only execute if the condition x > 5 is True.

Example - Letter Grade (if). In Listing 14.1, we define a function that returns "A", "B", "C", or "D" if a score falls within the following numerical ranges:

  • If score is between 90 and 100, return "A".
  • If score is between 80 and 89, return "B".
  • If score is between 70 and 79, return "C".
  • If score is between 0 and 69, return "D".
Listing 14.1: Example of an if statement to convert a score to a letter grade. In Listing 14.1 (a), we use the and operator to check multiple conditions. In Listing 14.1 (b), we use chain comparisons to simplify the code. Chain comparisons are a concise way to check if a variable is within a range.
(a) Example of if statements to convert a score to a letter grade.
def letter_grade(score):
    if score >= 90 and score <= 100:
        return "A"
    if score >= 80 and score < 90:
        return "B"
    if score >= 70 and score < 80:
        return "C"
    if score >= 0 and score < 70:
        return "D"
(b) Example of if statements to convert a score to a letter grade using chain comparisons.
def letter_grade(score):
    if 90 <= score <= 100:
        return "A"
    if 90 > score >= 80:
        return "B"
    if 80 > score >= 70:
        return "C"
    if 70 > score >= 0:
        return "D"

How could we test the function letter_grade? In Listing 14.1, we define a test function that checks if the function returns the correct grade for a given score. We are particularly interested in the boundaries between grades.

Listing 14.2: Testing the letter_grade function to check if it returns the correct grade for a given score. The boundaries (i.e., the points where the grade changes) are typically the most critical points to test because they are more likely to contain bugs.
def test_letter_grade():
    assert letter_grade(90) == "A"
    assert letter_grade(91) == "A"
    assert letter_grade(89) == "B"
    assert letter_grade(81) == "B"
    assert letter_grade(80) == "B"
    assert letter_grade(79) == "C"
    assert letter_grade(71) == "C"
    assert letter_grade(70) == "C"
    assert letter_grade(69) == "D"
    assert letter_grade(0) == "D" 

🤔 The letter_grade in Listing 14.1 is not perfect. What happens if the score is negative?

14.4 if-else Statement (Two-Way if)

An if-else statement allows us to handle two cases based on a condition. If the condition is True, one block of code is executed; otherwise, another. The syntax of an if-else statement is as follows:

if condition:
    # Code block to execute if condition is True
else:
    # Code block to execute if condition is False
# Code block outside the if-else statement

Example - Greater or Not. In Listing 14.3, we define a function that prints a message based on the value of x and y. If x is greater than y, the message "{x} is greater than {y}" will be printed. Otherwise, the message "{x} is not greater than {y}" will be printed.

Listing 14.3: Example of an if-else statement.
def x_greater_than_y(x, y):
    # Only one block within the conditions will trigger
    if x > y:
        print(f"{x} is greater than {y}")
        return True
    else:
        print(f"{x} is not greater than {y}")
        return False

# Test the function
assert x_greater_than_y(5, 3) == True
assert x_greater_than_y(3, 5) == False
5 is greater than 3
3 is not greater than 5

Example - Even or Odd. In Listing 14.4, we define a function that returns "Even" if the number is even and "Odd" if it’s odd.

Listing 14.4: Function to check if a number is even or odd using an if-else statement. Only one block within the conditions will trigger.
def check_even_odd(num):
    if num % 2 == 1:
        return "Odd"
    else:
        return "Even"

assert check_even_odd(4) == "Even"
assert check_even_odd(7) == "Odd"

Sometimes, the else block can be omitted in the function body. In Listing 14.5, we define a function that checks if a number is even or odd using an if statement without the else block. If the condition is True, the return statement is executed, and the function exits. If the condition is False, the function continues to the next statement after the if block.

Listing 14.5: Function to check if a number is even or odd using an if-else statement without the else block. Only one block within the conditions will trigger.
def check_even_odd(num):
    if num % 2 == 1:
        return "Odd"
    return "Even"

assert check_even_odd(4) == "Even"
assert check_even_odd(7) == "Odd"

In Listing 14.6, we review the letter_grade function and add a condition to handle invalid grades (i.e., scores less than 0 or greater than 100). We return None for invalid grades.

Listing 14.6: Function to determine the grade based on the score using an if-else statement. The function returns None for invalid grades.
def letter_grade(score):
    if score >= 90 and score <= 100:
        return "A"
    if 90 > score >= 80:
        return "B"
    if 80 > score >= 70:
        return "C"
    if 70 > score >= 0:
        return "D"
    return None

assert letter_grade(105) == None
assert letter_grade(-5) == None

14.5 Nested if Statements

Nested if statements allow us to check multiple conditions by nesting one if within another. It provides a way to handle more complex decision trees. The syntax of a nested if statement is as follows:

if condition1:
    if condition2:
        # Block 1 (condition1 and condition2 are True)
    else:
        # Block 2 (condition1 is True and condition2 is False)
else:
    if condition2:
        # Block 3 (condition1 is False and condition2 is True)
    else:
        # Block 4 (condition1 and condition2 are False)
# Code block outside the nested if statement

Notice that the same structure could be alternatively represented as:

if condition1 and condition2:
    # Block 1 (condition1 and condition2 are True)
if condition1 and not condition2:
    # Block 2 (condition1 is True and condition2 is False)
if not condition1 and condition2:
    # Block 3 (condition1 is False and condition2 is True)
if not condition1 and not condition2:
    # Block 4 (condition1 and condition2 are False)
# Code block outside the nested if statement

Example - Interval Formatting. In Listing 14.7, we define a function that formats an interval based on the start and end values and whether the interval is inclusive or exclusive. The function returns the formatted interval as a string.

Listing 14.7: Function to generate a formatted interval based on the start and end values and whether the interval is inclusive or exclusive using nested if statements.
def format_interval(
    start_value, 
    end_value, 
    is_start_inclusive=True, 
    is_end_inclusive=True
):
    if start_value > end_value:
        return "Error: Invalid Range"

    if is_start_inclusive:
        if is_end_inclusive:
            return f"[{start_value}, {end_value}]"
        else:
            return f"[{start_value}, {end_value})"
    else:
        if is_end_inclusive:
            return f"({start_value}, {end_value}]"
        else:
            return f"({start_value}, {end_value})"

# Assertions for validation
assert format_interval(1, 5) == "[1, 5]"
assert format_interval(1, 5, is_start_inclusive=False) == "(1, 5]"
assert format_interval(1, 5, is_end_inclusive=False) == "[1, 5)"
assert format_interval(
    1, 5, 
    is_start_inclusive=False,
    is_end_inclusive=False
) == "(1, 5)"
assert format_interval(5, 1) == "Error: Invalid Range"

14.6 if-elif-else Statement (Multi-Way if)

The if-elif-else statement allows us to check multiple conditions sequentially and execute the first one that evaluates to True. It’s useful for handling multiple cases. The elif stands for “else if.”

The syntax of an if-elif-else statement is as follows:

if condition1:
    # Code block to execute if condition1 is True
elif condition2:
    # Code block to execute if condition2 is True
elif condition3:
    # Code block to execute if condition3 is True
else:
    # Code block to execute if none of the conditions are True
# Code block outside the if-elif-else statement

Example - Letter Grade (if-elif-else). In Listing 14.8, we review the letter_grade function and rewrite it using an if-elif-else statement.

Listing 14.8: Function to determine the grade based on the score using an if-elif-else statement. Only one block within the conditions will trigger.
def letter_grade(score):
    if score < 0 or score > 100:
        return "Invalid Grade"
    elif score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    else:
        return "D"

assert letter_grade(95) == "A"
assert letter_grade(85) == "B"
assert letter_grade(75) == "C"
assert letter_grade(65) == "D"
assert letter_grade(-5) == "Invalid Grade"
assert letter_grade(105) == "Invalid Grade"

14.7 match Statement

Python 3.10 introduced the match statement, which can be used to perform pattern matching. It allows you to match a value against a series of patterns and execute the corresponding block of code. Sometimes, it can be more concise and readable than using if-elif-else statements. The syntax is shown in Listing 14.9.

Listing 14.9: Syntax of the match statement in Python. The case _: is the default case that executes when no other pattern matches. Only one block within the conditions will trigger. The cases are evaluated in order, and the first matching case is executed.
match value:
    case pattern1:
        # Code block for pattern1
    case pattern2:
        # Code block for pattern2
    case _:
        # Default case

A pattern can be:

  • A literal value (e.g., 1, "apple")
  • A variable (e.g., x, name)
  • A wildcard (_) to match any value
  • A sequence of patterns (e.g., (1, 2))
  • A combination of literals (e.g., 1 | 2)

See the Python documentation for more details on the match statement.

Example - Week Day. In Listing 14.10, we define a function that returns the name of the day of the week based on the day number.

Listing 14.10: Function to determine the day of the week based on the day number using a match statement. Only one block within the conditions will trigger.
def week_day(day):
    match day:
        case 1: return "Monday"
        case 2: return "Tuesday"
        case 3: return "Wednesday"
        case 4: return "Thursday"
        case 5: return "Friday"
        case 6: return "Saturday"
        case 7: return "Sunday"
        case _: return "Invalid Day"

# Validation
assert week_day(1) == "Monday"
assert week_day(4) == "Thursday"
assert week_day(7) == "Sunday"
assert week_day(0) == "Invalid Day"

Example - Categorizing Cars. In Listing 14.11, we define a function that categorizes a car into one of four categories based on whether it’s electric and sporty. The function takes two Boolean parameters, is_electric and is_sporty, and returns the car category:

  • "Electric Sportscar": is_electric is True and is_sporty is True
  • "Electric Car": is_electric is True and is_sporty is False
  • "Sportscar": is_electric is False and is_sporty is True
  • "Regular Car": is_electric is False and is_sporty is False
Listing 14.11: Function to categorize cars based on whether they are electric and sporty using a match statement.
def categorize_car(is_electric, is_sporty):
    match (is_electric, is_sporty):
        case (True, True): return "Electric Sportscar"
        case (True, False): return "Electric Car"
        case (False, True): return "Sportscar"
        case (False, False): return "Regular Car"
        case _: return "Invalid Input"

# Validation
assert categorize_car(True, True) == "Electric Sportscar"
assert categorize_car(True, False) == "Electric Car"
assert categorize_car(False, True) == "Sportscar"
assert categorize_car(False, False) == "Regular Car"

Example - Month to Quarter. In Listing 14.12, we define a function that converts a month number to the corresponding quarter of the year. We use | (named “pipe”) to combine multiple values in a pattern. The pipe operator in programming is usually used to denote a logical OR operation.

Listing 14.12: Function to convert a month number to the corresponding quarter of the year using a match statement.
def month_to_quarter(month):
    match month:
        case 1 | 2 | 3: return "Q1"
        case 4 | 5 | 6: return "Q2"
        case 7 | 8 | 9: return "Q3"
        case 10 | 11 | 12: return "Q4"
        case _: return "Invalid Month"

# Validation
assert month_to_quarter(1) == "Q1"
assert month_to_quarter(4) == "Q2"
assert month_to_quarter(7) == "Q3"
assert month_to_quarter(10) == "Q4"
assert month_to_quarter(13) == "Invalid Month"

14.8 Truthy and Falsy Values

In Python, values can be evaluated in a boolean context. This means that they can be used as conditions in if statements.

Truthy Values. Values that evaluate to True in a boolean context are called truthy values. For example:

  • True
  • Any non-zero number
  • Any non-empty string
  • Any non-empty collection (list, tuple, dictionary, set)

Falsy Values. Values that evaluate to False in a boolean context are called falsy values. For example:

  • False
  • None
  • 0, 0.0, 0j (zero as an integer, float, or complex number)
  • "" (empty string)
  • [], (), {}, set() (empty list, tuple, dictionary, set)

Example - Checking if a List is Empty. In Listing 14.13, we define a function that checks if a list is empty using the list itself in an if statement.

Listing 14.13: Example of checking if a list is empty using the list itself in an if statement.
def is_list_empty(lst):
    if lst:
        return False
    else:
        return True

# Test the function
assert is_list_empty([]) == True
assert is_list_empty([1, 2, 3]) == False

Example - Checking if a String is Empty. In Listing 14.14, we define a function that checks if a string is empty using the string itself in an if statement.

Listing 14.14: Example of checking if a string is empty using the string itself in an if statement.
def is_string_empty(s):
    if s:
        return False
    else:
        return True

# Test the function
assert is_string_empty("") == True
assert is_string_empty("Hello") == False

Example - Checking if a Number is Non-Zero. In Listing 14.15, we define a function that checks if a number is non-zero using the number itself in an if statement.

Listing 14.15: Example of checking if a number is non-zero using the number itself in an if statement.
def is_number_nonzero(num):
    if num:
        return True
    else:
        return False

# Test the function
assert is_number_nonzero(0) == False
assert is_number_nonzero(5) == True

Notice that throughot Listing 14.13, Listing 14.14, and Listing 14.15, the if statement evaluates the value in a boolean context, without the need to compare it to True or False. Comparing a value to True or False is redundant and not Pythonic as illustrated in Figure 14.2.

Figure 14.2: We don’t need to compare a value to True or False in an if statement. Python evaluates the value in a boolean context.

14.9 Conditional Expressions (Ternary Operator)

Python supports a concise way to write conditional expressions using the ternary operator (also known as a conditional expression). The syntax of a ternary operator is as follows:

value_if_true if condition else value_if_false

The ternary operator is a one-liner that allows you to evaluate a condition and return one of two values based on the result. It’s a compact way to write simple if-else statements.

Example - Absolute Value. In Listing 14.16, we define a function that calculates the absolute value of a number using the ternary operator.

Listing 14.16: Example of calculating the absolute value of a number using the ternary operator.
def absolute_value(num):
    return num if num >= 0 else -num
    
# Test the function
assert absolute_value(5) == 5
assert absolute_value(-3) == 3

Example - Maximum of Two Numbers. In Listing 14.17, we define a function that returns the maximum of two numbers using the ternary operator.

Listing 14.17: Example of finding the maximum of two numbers using the ternary operator.
def max_of_two(x, y):
    return x if x > y else y

# Test the function
assert max_of_two(5, 3) == 5
assert max_of_two(3, 5) == 5

14.10 Short-Circuit Evaluation

In Python, logical operators (and, or) use short-circuit evaluation. This means that the evaluation of the expression stops as soon as the result is determined (see the docs).

14.10.1 Short-Circuiting with and

When using the and operator:

  • If the first expression is False, the result is False, and the second, third, etc., expressions are not evaluated.
  • If the first expression is True, the result depends on the second expression. If the second expression is True, the result depends on the third expression, and so on.

Example - Short-Circuiting with and. In Listing 14.18, we define a function that calls another function only if the input is not None.

Listing 14.18: Example of short-circuiting with the and operator. The is_valid function is only called if the password is not None.
def is_valid(password: str) -> bool:
    """Check if the password is valid.

    A valid password must:
    - Be at least 8 characters long
    - Not be all lowercase or all uppercase

    Parameters:
    ----------
    password (str): The password to check.

    Returns:
    -------
    bool: True if the password is valid, False otherwise.
    """
    print(f"Checking password {password}. ", end="")
    return (
        len(password) >= 8
        and password.lower() != password
        and password.upper() != password
    )

def check_password(password):
    if password and is_valid(password):
        print("Password is valid!")
    else:
        print("Invalid password!")

# Test the function
check_password("Password123")
check_password("password")
check_password(None)
check_password("")
Checking password Password123. Password is valid!
Checking password password. Invalid password!
Invalid password!
Invalid password!

14.10.2 Short-Circuiting with or

When using the or operator:

  • If the first expression is True, the result is True, and the second, third, etc., expressions are not evaluated.
  • If the first expression is False, the result depends on the second expression. If the second expression is False, the result depends on the third expression, and so on.

Example - Short-Circuiting with or. In Listing 14.19, we define a function that evaluated the results of three expensive functions and returns True if any of them is True.

Listing 14.19: Example of short-circuiting with the or operator. The functions f1, f2, and f3 are only evaluated as needed, that is, until the first True result is found.
from time import sleep, time

def f1(num):
    print("Evaluating f1.")
    sleep(1)
    return num % 2 == 0

def f2(num):
    print("Evaluating f2.")
    sleep(1)
    return num % 2 == 0

def f3(num):
    print("Evaluating f3.")
    sleep(1)
    return num % 2 == 0

def check_any_condition(n1, n2, n3):
    return f1(n1) or f2(n2) or f3(n3)

# Measure the time taken to evaluate the function
print("Only the first function should be evaluated.")
start_time = time()
check_any_condition(2, 2, 1)
end_time = time()
print(f"Time taken: {end_time - start_time:.2f} seconds")

print("Only the first two functions should be evaluated.")
start_time = time()
check_any_condition(1, 2, 2)
end_time = time()
print(f"Time taken: {end_time - start_time:.2f} seconds")

print("All functions should be evaluated.")
start_time = time()
check_any_condition(1, 1, 1)
end_time = time()
print(f"Time taken: {end_time - start_time:.2f} seconds")
Only the first function should be evaluated.
Evaluating f1.
Time taken: 1.00 seconds
Only the first two functions should be evaluated.
Evaluating f1.
Evaluating f2.
Time taken: 2.00 seconds
All functions should be evaluated.
Evaluating f1.
Evaluating f2.
Evaluating f3.
Time taken: 3.00 seconds

🤔 What are the states of the call stack when executing Listing 14.19?

14.11 Conditional Statements for Error Handling?

In Python, conditional statements can be used for error handling. For example, you can check if a file exists before reading it, or if a value is None before using it. However, Python provides a more robust way to handle errors using exceptions. Exceptions are a way to handle errors that occur during the execution of a program. They allow you to gracefully handle errors and exceptions that may arise during the execution of your code.

In Listing 14.20, we compare two ways to handle a division by zero error. The first function uses an if statement to check if the denominator is zero before performing the division. The second function uses a try-except block to catch the ZeroDivisionError exception that occurs when dividing by zero.

Listing 14.20: Example of handling a division by zero error using an if statement and a try-except block.
(a) Preventing a division by zero error using an if statement to check if the denominator is zero before performing the division.
def divide_v1(
        numerator,
        denominator
    ):
    if denominator == 0:
        return None
    return numerator / denominator

# Test the function
assert divide_v1(10, 2) == 5
assert divide_v1(10, 0) == None
(b) Handling the ZeroDivisionError exception using a try-except block. When an exception occurs, the code inside the except block is executed.
def divide_v2(
        numerator,
        denominator
    ):
    try:
        return numerator / denominator
    except ZeroDivisionError:
        return None

# Test the function
assert divide_v2(10, 2) == 5
assert divide_v2(10, 0) == None

In general, it’s recommended to use exceptions for error handling in Python since they provide a more robust and flexible way to handle errors. Exceptions allow you to separate error-handling code from the normal flow of your program, making your code cleaner and easier to read. In Listing 14.20, the try-except block can be further extended to handle other exceptions that may occur during the division operation, for example, TypeError if the input is not a number. For example, in Listing 14.21, we extend the divide_v2 function to handle both ZeroDivisionError and TypeError exceptions.

Listing 14.21: Extending the divide_v2 function to handle both ZeroDivisionError and TypeError exceptions using a try-except block.
def divide_v2(
        numerator,
        denominator
    ):
    try:
        return numerator / denominator
    except ZeroDivisionError:
        print("Cannot divide by zero.")
        return None
    except TypeError:
        print("Invalid input type.")
        return None
    except ValueError:
        print("Invalid value.")
        return None

# Test the function
assert divide_v2(10, 2) == 5
assert divide_v2(10, 0) == None
assert divide_v2(10, "2") == None

# Test division by infinity
import math
assert divide_v2(10, math.inf) == 0

# Test division by very small number (close to zero)
assert divide_v2(10, 1e-1000) == None
Cannot divide by zero.
Invalid input type.
Cannot divide by zero.

14.12 Isn’t AI Just a Bunch of if-else Statements?

On the surface, artificial intelligence (AI) may seem like a series of nested if-else statements: given input data, an AI model will output a certain prediction or decision. The big diffence is on how the output is generated. In the case of if-else statements, the logic is explicitly defined by the programmer. The domain knowledge is encoded in the form of conditions and rules, and the program follows these rules to make decisions.

As for AI models, they can handle more complex logic by learning the conditions from large amounts of data. This is done through a process called machine learning, where the model learns patterns and relationships in the data to make predictions or decisions. It would be extremely difficult, for example, creating a decision tree for driving autonomous cars using nested if-else statements due to the complexity and large scope of the rules. Using AI, the model can learn from examples of driving behavior and road conditions to make decisions in real-time.

Figure 14.3: Isn’t artificial intelligence (AI) just a series of nested if statements? In some sense, yes. But AI can handle more complex logic by learning from large amounts of data. For example, it would be impractical to write a decision tree for recognizing objects in images using nested If, Else If, and Else statements. Nowadays, labeling images and training models to recognize objects is done using machine learning algorithms.

14.13 Exercises

14.13.1 Is Even

Write a Python function that takes an integer as input and returns True if the number is even and False otherwise. Use only if statements.

def is_even(num) -> bool:
    pass # Add code here!

def test_is_even():
    assert is_even(4) == True
    assert is_even(7) == False
    assert is_even(0) == True

14.13.2 Categorize Number

Write a Python function that takes an integer as input and returns:

  • "Positive" if it’s positive,
  • "Negative" if it’s negative, and
  • "Zero" if it’s zero,

using only if statements.

def categorize_number(num):
    pass # Add code here!

def test_categorize_number():
    assert categorize_number(5) == "Positive"
    assert categorize_number(-3) == "Negative"
    assert categorize_number(0) == "Zero"

14.13.3 Determining Quadrant

Write a function that takes two numbers (x and y) as input representing coordinates on a 2D plane. The function should return the quadrant in which the point lies ("Quadrant I", "Quadrant II", "Quadrant III", or "Quadrant IV") or "Origin" if the point is at the origin (0, 0). Use only if statements without else.

def determine_quadrant(x, y):
    pass # Add code here!

def test_determine_quadrant():
    assert determine_quadrant(3, 4) == "Quadrant I"
    assert determine_quadrant(-3, 4) == "Quadrant II"
    assert determine_quadrant(-3, -4) == "Quadrant III"
    assert determine_quadrant(3, -4) == "Quadrant IV"
    assert determine_quadrant(0, 0) == "Origin"

14.13.4 Vowel or Consonant

Write a function that takes a single character (a letter) as input and returns "Vowel" if it’s a vowel, namely, "a", "e", "i", "o", or "u", or "Consonant" otherwise. Create a version of the function using all Python’s conditional statements:

  • if
  • if-else
  • if-elif-else
  • match
  • Ternary operator
def is_vowel_or_consonant(char):
    pass # Add code here!

def test_is_vowel_or_consonant():
    assert is_vowel_or_consonant("a") == "Vowel"
    assert is_vowel_or_consonant("b") == "Consonant"
    assert is_vowel_or_consonant("i") == "Vowel"

14.13.5 Checking Divisibility

Write a Python function that takes two integers as input and returns "Divisible" if the first number is divisible by the second number, and "Not Divisible" otherwise.

def check_divisibility(num1, num2):
    pass # Add code here!

def test_check_divisibility():
    assert check_divisibility(12, 4) == "Divisible"
    assert check_divisibility(7, 3) == "Not Divisible"
    assert check_divisibility(20, 7) == "Not Divisible"

14.13.6 Checking Age Eligibility

Write a Python function that takes an age as input and returns "Eligible" if the age is 18 or older, and "Not Eligible" otherwise.

def check_age_eligibility(age):
    pass # Add code here!

def test_check_age_eligibility():
    assert check_age_eligibility(25) == "Eligible"
    assert check_age_eligibility(17) == "Not Eligible"
    assert check_age_eligibility(18) == "Eligible"

14.13.7 Checking Valid Username

Write a Python function that takes a username as input and returns "Valid" if the username contains at least 6 characters and does not contain spaces, and "Invalid" otherwise.

def check_username_validity(username):
    pass # Add code here!

def test_check_username_validity():
    assert check_username_validity("john_doe") == "Valid"
    assert check_username_validity("user name") == "Invalid"
    assert check_username_validity("short") == "Invalid"

14.13.8 Categorizing BMI

Create a function that categorizes a person’s Body Mass Index (BMI) into "Underweight", "Normal", "Overweight", or "Obese". The criteria is as follows:

  • Underweight: BMI < 18.5
  • Normal: 18.5 <= BMI <= 24.9
  • Overweight: 25 <= BMI <= 29.9
  • Obese: BMI >= 30
  • Invalid BMI: BMI < 0
def categorize_bmi(bmi):
    pass # Add code here!

def test_categorize_bmi():
    assert categorize_bmi(22.5) == "Normal"
    assert categorize_bmi(29) == "Overweight"
    assert categorize_bmi(31.7) == "Obese"
    assert categorize_bmi(-5) == "Invalid BMI"

14.13.9 Number Sign

Write a function that takes two integers as input and returns "Positive" if both are positive, "Negative" if both are negative, and "Mixed" otherwise.

def check_number_sign(num1, num2):
    pass # Add code here!

def test_check_number_sign():
    assert check_number_sign(5, 3) == "Positive"
    assert check_number_sign(-2, -4) == "Negative"
    assert check_number_sign(7, -1) == "Mixed"

14.13.10 Increasing / Decreasing Order

Write a function that takes three numbers as input and returns "Increasing" if they are in increasing order, "Decreasing" if they are in decreasing order, and "Neither" otherwise.

def check_number_order(num1, num2, num3):
    pass # Add code here!

def test_check_number_order():
    assert check_number_order(1, 2, 3) == "Increasing"
    assert check_number_order(3, 2, 1) == "Decreasing"
    assert check_number_order(2, 1, 3) == "Neither"

14.13.11 Triangle Type

Write a function that takes three integers as input representing the sides of a triangle and returns "Equilateral" if all sides are equal, "Isosceles" if two sides are equal, and "Scalene" if all sides are different.

def check_triangle_type(side1, side2, side3):
    pass # Add code here!

def test_check_triangle_type():
    assert check_triangle_type(5, 5, 5) == "Equilateral"
    assert check_triangle_type(4, 4, 3) == "Isosceles"
    assert check_triangle_type(3, 4, 5) == "Scalene"

14.13.12 Checking Leap Year

Write a function that takes a year as input and returns "Leap Year" if it’s a leap year and "Not a Leap Year" if it’s not.

def check_leap_year(year):
    pass # Add code here!

def test_check_leap_year():
    assert check_leap_year(2020) == "Leap Year"
    assert check_leap_year(2023) == "Not a Leap Year"
    assert check_leap_year(2000) == "Leap Year"

14.13.13 Checking Validity of a Date

Write a function that takes three integers as input representing a date (day, month, year) and returns

  • "Valid" if the date is valid, or
  • "Invalid", otherwise.

Make sure you consider leap years!

def check_date_validity(day, month, year):
    # Check if year is valid
    # Check if month is valid
    # If month has 30 days, check if day <= 30
    # If month has 31 days, check if day <= 31
    # If month is Feb and the year is a leap year, day <=29
    # If month is Feb and the year is not a leap year, day <=28

def test_check_date_validity():
    assert check_date_validity(31, 12, 2023) == "Valid"
    assert check_date_validity(29, 2, 2024) == "Valid"
    assert check_date_validity(30, 2, 2024) == "Invalid"
    # Add more edge cases here!

14.13.14 Multiple Conditions with Strings

Write a function that takes two strings as input and returns "Equal" if they are the same, "Different" if they are different, and "CaseSensitive" if they are different considering case.

def compare_strings(str1, str2):
    pass # Add code here!

def test_compare_strings():
    assert compare_strings("hello", "Hello") == "CaseSensitive"
    assert compare_strings("openai", "openai") == "Equal"
    assert compare_strings("Test", "test") == "CaseSensitive"

14.13.15 Checking Password Validity

Write a function that takes a password as input and returns "Valid" if the password meets the following criteria:

  1. Contains at least one uppercase letter.
  2. Contains at least one lowercase letter.
  3. Contains at least one numeric digit.
  4. Contains at least one special character (!@#$%^&*()_+[]{}|;:'",.<>?).
  5. Has a minimum length of 8 characters.

Return "Invalid" if any of these criteria are not met.

def check_password_validity(password):
    pass # Add code here!

def test_check_password_validity():
    assert check_password_validity("Password123!") == "Valid"
    assert check_password_validity("WeakPass") == "Invalid"
    assert check_password_validity("SecurePass123") == "Invalid"
    assert check_password_validity("Complex@Pswd123") == "Valid"

14.13.16 Checking Password Validity (Using Subfunctions)

Redo the function check_password_validity using the following subfunctions to test the criteria:

  • has_uppercase(password)
  • has_lowercase(password)
  • has_digit(password)
  • has_special_char(password)
  • has_min_length(password, min_length)

Also, write a test function with the edge cases for each subfunction and a test function for check_password_validity_clean.

def has_uppercase(password):
    pass # Add code here!

def has_lowercase(password):
    pass # Add code here!

def has_digit(password):
    pass # Add code here!

def has_special_char(password):
    pass # Add code here!

def has_min_length(password, min_length):
    pass # Add code here!

def check_password_validity_clean(password):
    pass # Add code here!

def test_subfunctions():
    # Add test cases for each subfunction

def test_check_password_validity_clean():
    # Add test cases for check_password_validity_clean

14.13.17 Convert Letter Grade

Write a Python function that takes a numeric grade (0 to 100) as input and returns the corresponding letter grade ("A", "B", "C", "D", or "F") using if-elif-else statements.

The grading scale is as follows:

  • If the grade is between 90 and 100, return "A".
  • If the grade is between 80 and 89.9, return "B".
  • If the grade is between 70 and 79.9, return "C".
  • If the grade is between 60 and 69.9, return "D".
  • If the grade is less than 60, return "F".

If the grade is outside the range 0 to 100, return "Invalid Grade".

def convert_to_letter_grade(grade):
    pass # Add code here!

def test_convert_to_letter_grade():
    assert convert_to_letter_grade(92) == "A"
    assert convert_to_letter_grade(78) == "C"
    assert convert_to_letter_grade(55) == "F"
    assert convert_to_letter_grade(105) == "Invalid Grade"

14.13.18 Categorizing Blood Pressure

Write a function that takes systolic and diastolic blood pressure readings as input and returns a category ("Normal", "Prehypertension", "Hypertension Stage 1", "Hypertension Stage 2", or "Hypertensive Crisis") using if-elif-else statements.

Blood Pressure Categories:

Blood Pressure Category Systolic Range Diastolic Range
Normal Below 120 Below 80
Prehypertension 120–129 Below 80
Hypertension Stage 1 130–139 80–89
Hypertension Stage 2 140 and above 90 and above
Hypertensive Crisis Above 180 Above 120
def categorize_blood_pressure(systolic, diastolic):
    pass # Add code here!

def test_categorize_blood_pressure():
    assert categorize_blood_pressure(110, 70) == "Normal"
    assert categorize_blood_pressure(125, 85) == "Invalid Blood Pressure"
    assert categorize_blood_pressure(150, 95) == "Hypertension Stage 2"
    assert categorize_blood_pressure(190, 130) == "Hypertensive Crisis"
    assert categorize_blood_pressure(134, 85) == "Hypertension Stage 1"

14.14 References