Python & AI Tutorials Logo
Python Programming

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.

python
# 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: False

Think of and as a strict gatekeeper: all conditions must pass for the overall check to succeed.

Truth Table for and:

Left OperandRight OperandResult
TrueTrueTrue
TrueFalseFalse
FalseTrueFalse
FalseFalseFalse

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.

python
# 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: False

Think of or as a lenient gatekeeper: you only need to satisfy one condition to pass.

Truth Table for or:

Left OperandRight OperandResult
TrueTrueTrue
TrueFalseTrue
FalseTrueTrue
FalseFalseFalse

Here's a practical example for a discount eligibility system:

python
# 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: False

The 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.

python
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: False

Truth Table for not:

OperandResult
TrueFalse
FalseTrue

The not operator is particularly useful when you want to check the opposite of a condition:

python
# 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: True

9.1.4) Combining Multiple Logical Operators

You can combine multiple logical operators in a single expression to build more sophisticated conditions:

python
# 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: True

Let's trace through this evaluation:

  1. order_total >= 50 evaluates to False (45.00 is not >= 50)
  2. is_premium is True
  3. False or True evaluates to True
  4. in_stock is True
  5. True and True evaluates to True

Here's another example with access control:

python
# 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: True

Notice 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):

  1. not (highest precedence)
  2. and (middle precedence)
  3. or (lowest precedence)

This means not is evaluated first, then and, and finally or.

python
# 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 → True

Let's see this step by step with a more detailed example:

python
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: False

9.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.

python
# 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: True

These 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.

python
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 → True

9.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.

python
# 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 False

Here's a practical example showing why short-circuit evaluation matters:

python
# 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:

python
# 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 range

9.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.

python
# 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 True

9.3.3) Practical Applications of Short-Circuit Evaluation

Avoiding Errors:

python
# 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:

python
# 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 age

No

Yes

Yes

No

and Expression

Left Side True?

Return False
Skip Right Side

Evaluate Right Side

Return Right Result

or Expression

Left Side True?

Return True
Skip Right Side

Evaluate Right Side

Return Right Result

9.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):

  • False
  • None
  • 0 (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
python
# 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 truthy

9.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, and returns the left operand (without evaluating the right)
  • If the left operand is truthy, and returns the right operand
python
# 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: None

Let's trace through these examples:

python
# 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: world

9.4.3) What or Actually Returns

Similarly, the or operator returns one of its operands:

  • If the left operand is truthy, or returns the left operand (without evaluating the right)
  • If the left operand is falsy, or returns the right operand
python
# 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: 0

Let's trace through these examples:

python
# 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: 0

9.4.4) Practical Uses of or for Default Values

One common pattern is using or to provide default values:

python
# 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: 5

This pattern works because if the left side is falsy (empty string, None, 0, etc.), or returns the right side (the default value).

and

or

Yes

No

Yes

No

Logical Operator with Non-Boolean

Operator Type

Left Falsy?

Left Truthy?

Return Left Value

Return Right Value

Return Left Value

Return Right 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 (True or False)
  • not converts the operand to a boolean, then negates it
python
# 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.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai