9. Combining Conditions with Boolean Logic
In Chapter 7, we learned about boolean values and simple conditions using comparison operators. In Chapter 8, we used these conditions to make decisions with if statements. But real-world programs often need to check multiple conditions at once. Should we grant access if the user has the right password and is logged in? Should we show a warning if the temperature is too hot or too cold? Should we proceed if the file is not empty?
Python provides three logical operators that let us combine and modify boolean values: and, or, and not. These operators are the building blocks for expressing complex decision-making logic in your programs.
9.1) Logical Operators and, or, and not
The three logical operators work with boolean values (or values that can be treated as booleans) to produce new boolean results.
9.1.1) The and Operator
The and operator returns True only when both operands are true. If either operand is false, the entire expression is false.
# Both conditions must be true
age = 25
has_license = True
can_rent_car = age >= 21 and has_license
print(can_rent_car) # Output: True
# If either condition is false, result is False
age = 18
can_rent_car = age >= 21 and has_license
print(can_rent_car) # Output: FalseThink of and as a strict gatekeeper: all conditions must pass for the overall check to succeed.
Truth Table for and:
| Left Operand | Right Operand | Result |
|---|---|---|
True | True | True |
True | False | False |
False | True | False |
False | False | False |
9.1.2) The or Operator
The or operator returns True when at least one operand is true. It only returns False when both operands are false.
# At least one condition must be true
is_weekend = True
is_holiday = False
can_sleep_in = is_weekend or is_holiday
print(can_sleep_in) # Output: True
# Both conditions false
is_weekend = False
is_holiday = False
can_sleep_in = is_weekend or is_holiday
print(can_sleep_in) # Output: FalseThink of or as a lenient gatekeeper: you only need to satisfy one condition to pass.
Truth Table for or:
| Left Operand | Right Operand | Result |
|---|---|---|
True | True | True |
True | False | True |
False | True | True |
False | False | False |
Here's a practical example for a discount eligibility system:
# Customer gets discount if they're a student OR a senior citizen
age = 68
is_student = False
gets_discount = is_student or age >= 65
print(f"Eligible for discount: {gets_discount}") # Output: Eligible for discount: True
# Another customer
age = 30
is_student = False
gets_discount = is_student or age >= 65
print(f"Eligible for discount: {gets_discount}") # Output: Eligible for discount: FalseThe first customer qualifies because they meet one of the criteria (senior citizen), even though they're not a student.
9.1.3) The not Operator
The not operator is a unary operator (it works on a single operand) that reverses a boolean value. It turns True into False and False into True.
is_raining = False
is_sunny = not is_raining
print(is_sunny) # Output: True
is_raining = True
is_sunny = not is_raining
print(is_sunny) # Output: FalseTruth Table for not:
| Operand | Result |
|---|---|
True | False |
False | True |
The not operator is particularly useful when you want to check the opposite of a condition:
# Check if a file is NOT empty
file_size = 0
is_empty = file_size == 0
is_not_empty = not is_empty
print(f"File has content: {is_not_empty}") # Output: File has content: False
# Check if user is NOT logged in
is_logged_in = False
needs_login_prompt = not is_logged_in
print(f"Show login prompt: {needs_login_prompt}") # Output: Show login prompt: True9.1.4) Combining Multiple Logical Operators
You can combine multiple logical operators in a single expression to build more sophisticated conditions:
# Online store: free shipping if order is over $50 OR customer is premium member
# AND items are in stock
order_total = 45.00
is_premium = True
in_stock = True
gets_free_shipping = (order_total >= 50 or is_premium) and in_stock
print(f"Free shipping: {gets_free_shipping}") # Output: Free shipping: TrueLet's trace through this evaluation:
order_total >= 50evaluates toFalse(45.00 is not >= 50)is_premiumisTrueFalse or Trueevaluates toTruein_stockisTrueTrue and Trueevaluates toTrue
Here's another example with access control:
# User can access admin panel if they're an admin
# AND (they're on the internal network OR using VPN)
is_admin = True
on_internal_network = False
using_vpn = True
can_access_admin = is_admin and (on_internal_network or using_vpn)
print(f"Can access admin panel: {can_access_admin}") # Output: Can access admin panel: TrueNotice the parentheses around (on_internal_network or using_vpn). These are important because they control the order of evaluation, just like parentheses in arithmetic expressions.
9.2) Operator Precedence in Boolean Expressions (Not, And, Or Order)
When you combine multiple logical operators without parentheses, Python follows specific precedence rules to determine the order of evaluation. Understanding these rules helps you write correct conditions and avoid subtle bugs.
9.2.1) The Precedence Hierarchy
Python evaluates logical operators in this order (highest to lowest precedence):
not(highest precedence)and(middle precedence)or(lowest precedence)
This means not is evaluated first, then and, and finally or.
# Without parentheses, precedence determines order
result = True or False and False
print(result) # Output: True
# How Python evaluates this:
# Step 1: False and False → False (and has higher precedence than or)
# Step 2: True or False → TrueLet's see this step by step with a more detailed example:
is_weekend = False
is_holiday = True
has_work = True
# Expression: not has_work or is_weekend and is_holiday
free_time = not has_work or is_weekend and is_holiday
# Evaluation order:
# Step 1: not has_work → not True → False
# Step 2: is_weekend and is_holiday → False and True → False
# Step 3: False or False → False
print(f"Has free time: {free_time}") # Output: Has free time: False9.2.2) Using Parentheses for Clarity
Even when you understand precedence rules, using parentheses makes your code clearer and prevents mistakes. Parentheses override the default precedence and make your intentions explicit.
# Ambiguous without parentheses
result = True or False and False
print(result) # Output: True
# Clear with parentheses - what did we really mean?
result = (True or False) and False
print(result) # Output: False
result = True or (False and False)
print(result) # Output: TrueThese two expressions produce different results! The parentheses completely change the meaning.
9.2.3) Comparison Operators and Logical Operators Together
Comparison operators (like <, >, ==, !=) have higher precedence than logical operators. This means comparisons are evaluated before logical operations.
age = 25
income = 50000
# No parentheses needed around comparisons
eligible = age >= 18 and income >= 30000
print(f"Eligible for loan: {eligible}") # Output: Eligible for loan: True
# Python evaluates it as:
# Step 1: age >= 18 → True
# Step 2: income >= 30000 → True
# Step 3: True and True → True9.3) Short-Circuit Evaluation
Python uses short-circuit evaluation when evaluating boolean expressions with and and or. This means Python stops evaluating as soon as it knows the final result, potentially skipping the evaluation of later operands. This behavior is both a performance optimization and a useful programming technique.
9.3.1) How and Short-Circuits
With the and operator, if the left operand is False, Python knows the entire expression must be False (because both operands must be true for and to return True). Therefore, Python doesn't evaluate the right operand at all.
# Simple demonstration
x = 5
result = x < 3 and x > 10
print(result) # Output: False
# Python's evaluation:
# Step 1: x < 3 → 5 < 3 → False
# Step 2: Since left side is False, don't evaluate x > 10
# Step 3: Return FalseHere's a practical example showing why short-circuit evaluation matters:
# Checking if a number is divisible - avoiding division by zero
numerator = 100
denominator = 0
# This is safe because of short-circuit evaluation
# If denominator is 0, the division never happens
is_divisible = denominator != 0 and numerator % denominator == 0
print(f"Is divisible: {is_divisible}") # Output: Is divisible: False
# Without short-circuit evaluation, this would cause an error:
# denominator = 0
# result = numerator % denominator # ZeroDivisionError!The expression denominator != 0 evaluates to False, so Python never evaluates numerator % denominator, which would cause a division by zero error.
Let's see another example with string operations:
# Safely checking string properties
text = ""
# Check if text is not empty AND first character is uppercase
# Safe because if text is empty, we never try to access text[0]
has_uppercase_start = len(text) > 0 and text[0].isupper()
print(f"Starts with uppercase: {has_uppercase_start}") # Output: Starts with uppercase: False
# If we tried this without the length check:
# text = ""
# result = text[0].isupper() # IndexError: string index out of range9.3.2) How or Short-Circuits
With the or operator, if the left operand is True, Python knows the entire expression must be True (because at least one operand being true is sufficient). Therefore, Python doesn't evaluate the right operand.
# Simple demonstration
x = 15
result = x > 10 or x < 5
print(result) # Output: True
# Python's evaluation:
# Step 1: x > 10 → 15 > 10 → True
# Step 2: Since left side is True, don't evaluate x < 5
# Step 3: Return True9.3.3) Practical Applications of Short-Circuit Evaluation
Avoiding Errors:
# Safely accessing list elements
numbers = [1, 2, 3]
index = 5
# Check if index is valid before accessing
is_valid = index < len(numbers) and numbers[index] > 0
print(f"Valid and positive: {is_valid}") # Output: Valid and positive: False
# Without short-circuit, this would crash:
# is_valid = numbers[index] > 0 # IndexError!Checking Multiple Conditions Efficiently:
# Form validation - stop at first error
email = "user@example.com"
password = "pass"
age = 25
# Check each requirement in order of likelihood to fail
valid_form = (
len(email) > 0 and # Quick check
"@" in email and # Quick check
len(password) >= 8 and # Quick check
age >= 18 # Quick check
)
print(f"Form valid: {valid_form}") # Output: Form valid: False
# Stops at password length check, doesn't evaluate age9.4) What the and and or Operators Return with Non-Boolean Operands, and Common Boolean Expression Pitfalls
So far, we've seen and, or, and not work with boolean values. But Python's logical operators have an interesting behavior: they can work with any values, not just True and False. Understanding this behavior helps you write more concise code and avoid common mistakes.
9.4.1) Understanding Truthiness and Falsiness (Review)
As we learned in Chapter 7, Python treats many non-boolean values as either "truthy" or "falsy" in boolean contexts:
Falsy values (treated as False):
FalseNone0(zero of any numeric type)""(empty string)[](empty list){}(empty dictionary)()(empty tuple)
Truthy values (treated as True):
True- Any non-zero number
- Any non-empty string
- Any non-empty collection
# Demonstrating truthiness
if "hello":
print("Non-empty strings are truthy") # Output: Non-empty strings are truthy
if 0:
print("This won't print") # Zero is falsy
else:
print("Zero is falsy") # Output: Zero is falsy
if [1, 2, 3]:
print("Non-empty lists are truthy") # Output: Non-empty lists are truthy9.4.2) What and Actually Returns
The and operator doesn't always return True or False. Instead, it returns one of its operands:
- If the left operand is falsy,
andreturns the left operand (without evaluating the right) - If the left operand is truthy,
andreturns the right operand
# and returns the first falsy value, or the last value if all are truthy
result = 5 and 10
print(result) # Output: 10
result = 0 and 10
print(result) # Output: 0
result = "hello" and "world"
print(result) # Output: world
result = "" and "world"
print(result) # Output: (empty string)
result = None and "world"
print(result) # Output: NoneLet's trace through these examples:
# Example 1: Both truthy
result = 5 and 10
# Step 1: 5 is truthy, so evaluate right side
# Step 2: Return right side value: 10
print(result) # Output: 10
# Example 2: Left is falsy
result = 0 and 10
# Step 1: 0 is falsy, so return it immediately
# Step 2: Don't evaluate right side
print(result) # Output: 0
# Example 3: Both truthy strings
result = "hello" and "world"
# Step 1: "hello" is truthy, so evaluate right side
# Step 2: Return right side value: "world"
print(result) # Output: world9.4.3) What or Actually Returns
Similarly, the or operator returns one of its operands:
- If the left operand is truthy,
orreturns the left operand (without evaluating the right) - If the left operand is falsy,
orreturns the right operand
# or returns the first truthy value, or the last value if all are falsy
result = 5 or 10
print(result) # Output: 5
result = 0 or 10
print(result) # Output: 10
result = "" or "default"
print(result) # Output: default
result = "hello" or "world"
print(result) # Output: hello
result = None or 0
print(result) # Output: 0Let's trace through these examples:
# Example 1: Left is truthy
result = 5 or 10
# Step 1: 5 is truthy, so return it immediately
# Step 2: Don't evaluate right side
print(result) # Output: 5
# Example 2: Left is falsy
result = 0 or 10
# Step 1: 0 is falsy, so evaluate right side
# Step 2: Return right side value: 10
print(result) # Output: 10
# Example 3: Both falsy
result = None or 0
# Step 1: None is falsy, so evaluate right side
# Step 2: Return right side value: 0 (even though it's also falsy)
print(result) # Output: 09.4.4) Practical Uses of or for Default Values
One common pattern is using or to provide default values:
# User preferences with defaults
user_theme = "" # User hasn't set a theme
theme = user_theme or "light"
print(f"Theme: {theme}") # Output: Theme: light
user_theme = "dark"
theme = user_theme or "light"
print(f"Theme: {theme}") # Output: Theme: dark
# Configuration values
max_retries = None # Not configured
retries = max_retries or 3
print(f"Retries: {retries}") # Output: Retries: 3
max_retries = 5
retries = max_retries or 3
print(f"Retries: {retries}") # Output: Retries: 5This pattern works because if the left side is falsy (empty string, None, 0, etc.), or returns the right side (the default value).
9.4.12) Summary of What Operators Return
Here's a comprehensive summary of what each logical operator returns:
and operator:
- Returns the first falsy operand
- If all operands are truthy, returns the last operand
- Uses short-circuit evaluation (stops at first falsy value)
or operator:
- Returns the first truthy operand
- If all operands are falsy, returns the last operand
- Uses short-circuit evaluation (stops at first truthy value)
not operator:
- Always returns a boolean (
TrueorFalse) notconverts the operand to a boolean, then negates it
# Demonstrating all three operators
print(5 and 10) # Output: 10 (both truthy, return last)
print(0 and 10) # Output: 0 (first falsy, return it)
print(5 or 10) # Output: 5 (first truthy, return it)
print(0 or 10) # Output: 10 (first falsy, evaluate second)
print(not 5) # Output: False (5 is truthy, not returns boolean)
print(not 0) # Output: True (0 is falsy, not returns boolean)
print(not "") # Output: True (empty string is falsy)
print(not "hello") # Output: False (non-empty string is truthy)Understanding these behaviors helps you write more concise and Pythonic code, but always prioritize clarity. If using these features makes your code harder to understand, it's better to be explicit.
In this chapter, we've explored how to combine simple conditions into complex boolean logic using Python's and, or, and not operators. We've learned about operator precedence, short-circuit evaluation, and the surprising behavior of logical operators with non-boolean values. We've also examined common pitfalls and best practices for writing clear, correct boolean expressions.
These tools allow you to express sophisticated decision-making logic in your programs. Combined with the if statements from Chapter 8, you can now handle virtually any conditional logic your programs require. In the next chapter, we'll explore conditional expressions, which provide a compact way to choose between two values based on a condition.