Python Comparison and Logical Operators

Week 2 Day 2: Control Flow Fundamentals

Introduction to Operators in Python

Welcome to our exploration of comparison and logical operators in Python! These operators are the fundamental building blocks that allow our programs to make decisions and evaluate conditions. If Python were a language (which it is!), then operators would be the verbs and adjectives that give it expressive power.

Imagine you're a judge in a courtroom. Comparison operators are like asking questions such as "Is the defendant taller than 6 feet?" while logical operators are like connecting multiple pieces of evidence: "Was the defendant at the scene AND do they have a motive?"

The code for this lesson can be found in the /week2/day2/comparison_logical_operators.py file in your course repository.

Comparison Operators

Comparison operators allow us to compare values and determine the relationship between them. Every comparison operation results in a boolean value: either True or False.

Types of Comparison Operators

Operator Description Example Result
== Equal to 5 == 5 True
!= Not equal to 5 != 3 True
> Greater than 7 > 3 True
< Less than 5 < 10 True
>= Greater than or equal to 5 >= 5 True
<= Less than or equal to 5 <= 10 True

Basic Examples

a = 10
b = 20
c = 10

# Equal to
print(a == b)  # False
print(a == c)  # True

# Not equal to
print(a != b)  # True

# Greater than
print(a > b)   # False

# Less than
print(a < b)   # True

# Greater than or equal to
print(a >= c)  # True

# Less than or equal to
print(a <= c)  # True

Comparison with Different Data Types

Python can compare different data types, but the behavior can sometimes be unexpected. Let's explore some examples:

# Numbers of different types
print(5 == 5.0)  # True - integer and float with same value are considered equal

# Strings are compared lexicographically (alphabetically)
print("apple" < "banana")  # True - 'a' comes before 'b'
print("apple" < "Apple")   # False - capital letters come before lowercase in ASCII

# Different data types
print("5" == 5)  # False - string '5' is not equal to integer 5
print(True == 1)  # True - True is equal to 1
print(False == 0)  # True - False is equal to 0

# None comparisons
print(None == 0)  # False
print(None == "")  # False
print(None == None)  # True

Think of comparing strings like looking up words in a dictionary: Python compares characters one by one based on their Unicode code point values.

Comparing Complex Data Structures

# Lists - compared element by element
print([1, 2, 3] == [1, 2, 3])  # True
print([1, 2, 3] == [1, 2, 4])  # False
print([1, 2, 3] < [1, 2, 4])   # True - the third element is smaller

# Dictionaries - compare if they have the same key-value pairs
print({'a': 1, 'b': 2} == {'b': 2, 'a': 1})  # True - order doesn't matter
print({'a': 1, 'b': 2} == {'a': 1, 'c': 3})  # False - different keys

Identity vs. Equality: is vs ==

Python provides two ways to compare objects:

a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)  # True - same values
print(a is b)  # False - different objects in memory
print(a is c)  # True - same object in memory

# Special case with small integers and strings
x = 5
y = 5
print(x is y)  # True - Python optimizes small integers

# But with larger numbers
large_x = 1000
large_y = 1000
print(large_x is large_y)  # May be False depending on implementation

# String interning
s1 = "hello"
s2 = "hello"
print(s1 is s2)  # True - Python may optimize common strings

Think of == as comparing the contents of two books, while is checks if they are literally the same physical book.

Real-World Example: Data Validation

def validate_user_input(username, password, age):
    """Validate user registration data."""
    
    errors = []
    
    # Username validation
    if len(username) < 3:
        errors.append("Username must be at least 3 characters long")
    
    if " " in username:
        errors.append("Username cannot contain spaces")
    
    # Password validation
    if len(password) < 8:
        errors.append("Password must be at least 8 characters long")
    
    if password.isalpha() or password.isdigit():
        errors.append("Password must contain both letters and numbers")
    
    # Age validation
    if not isinstance(age, (int, float)):
        errors.append("Age must be a number")
    elif age < 13:
        errors.append("You must be at least 13 years old to register")
    elif age > 120:
        errors.append("Please enter a valid age")
    
    # Return results
    if errors:
        return False, errors
    else:
        return True, ["Validation successful!"]

# Test the function
result, messages = validate_user_input("Alice123", "pass123word", 25)
print(f"Valid: {result}")
for message in messages:
    print(f"- {message}")

result, messages = validate_user_input("Bo", "password", 10)
print(f"Valid: {result}")
for message in messages:
    print(f"- {message}")

This example shows how comparison operators are essential for validating user input in real-world applications.

Logical Operators

Logical operators allow us to combine multiple conditions and create complex logical expressions. They operate on boolean values and return boolean results.

Types of Logical Operators

Operator Description Example Result
and True if both operands are true True and True True
or True if at least one operand is true True or False True
not Inverts the boolean value not True False

Truth Tables

Truth tables help us understand how logical operators behave with different combinations of inputs:

AND Operator
A B A and B
True True True
True False False
False True False
False False False
OR Operator
A B A or B
True True True
True False True
False True True
False False False
NOT Operator
A not A
True False
False True

Basic Examples

a = True
b = False

# AND operator
print(a and a)  # True
print(a and b)  # False
print(b and b)  # False

# OR operator
print(a or a)   # True
print(a or b)   # True
print(b or b)   # False

# NOT operator
print(not a)    # False
print(not b)    # True

Combining Logical Operators

a = True
b = False
c = True

# Complex expressions
print((a and b) or c)  # True
print(a and (b or c))  # True
print(not (a and b))   # True

# Using parentheses to control order of evaluation
print(not a and b)     # False - equivalent to (not a) and b
print(not (a and b))   # True - equivalent to not (False) = True

Just like in mathematics, you can use parentheses to control the order of operations. If you're uncertain about the precedence, it's always a good practice to use parentheses to make your code more readable.

Short-Circuit Evaluation

Python's logical operators use short-circuit evaluation, meaning they stop evaluating as soon as the result is determined.

# AND short-circuit: if first operand is False, second is not evaluated
def print_and_return_false():
    print("Evaluating first operand")
    return False

def print_and_return_true():
    print("Evaluating second operand")
    return True

# First operand is False, so second is never evaluated
print(print_and_return_false() and print_and_return_true())
# Output:
# Evaluating first operand
# False

# OR short-circuit: if first operand is True, second is not evaluated
print(print_and_return_true() or print_and_return_false())
# Output:
# Evaluating second operand
# True

Think of short-circuit evaluation like a chef who stops tasting a recipe as soon as they determine it won't work: if one crucial ingredient is spoiled, there's no need to taste the rest.

Using Short-Circuit Evaluation for Efficient Code

# Safe dictionary access
user_data = {"name": "Alice"}

# Without short-circuit
if "age" in user_data and user_data["age"] > 18:
    print("User is an adult")
else:
    print("Age unknown or user is not an adult")

# If we forget the existence check, this would raise a KeyError
if user_data.get("age") and user_data["age"] > 18:
    print("User is an adult")
else:
    print("Age unknown or user is not an adult")

In the first example, if "age" is not in the dictionary, the second part is never evaluated, preventing a KeyError.

Non-Boolean Operands in Logical Operations

Logical operators in Python don't always return boolean values! When used with non-boolean values, they return one of the operands:

# AND returns the first falsy value, or the last value if all are truthy
print(0 and 1)        # 0 (first falsy value)
print(1 and 2)        # 2 (last value, all truthy)
print([] and "hello") # [] (first falsy value)
print(42 and "")      # "" (first falsy value)
print("hi" and [1])   # [1] (last value, all truthy)

# OR returns the first truthy value, or the last value if all are falsy
print(0 or 1)         # 1 (first truthy value)
print(False or "")    # "" (last value, all falsy)
print([] or ())       # () (last value, all falsy)
print("hi" or 42)     # "hi" (first truthy value)
print(None or 0 or "default" or []) # "default" (first truthy value)

This behavior enables Pythonic idioms like providing default values:

def greet(name):
    # Use the provided name or default to "Guest" if name is falsy
    actual_name = name or "Guest"
    return f"Hello, {actual_name}!"

print(greet("Alice"))  # "Hello, Alice!"
print(greet(""))       # "Hello, Guest!"
print(greet(None))     # "Hello, Guest!"

Real-World Example: Access Control System

def check_access(user_id, resource_id, is_admin=False):
    """
    Determines if a user has access to a specific resource.
    
    Access is granted if any of these conditions are met:
    1. User is an admin
    2. User is the owner of the resource
    3. Resource is public AND the user has a verified account
    """
    # Simulate database lookup
    user = get_user_by_id(user_id)
    resource = get_resource_by_id(resource_id)
    
    if user is None or resource is None:
        return False, "User or resource not found"
    
    # Check if admin (short-circuit for efficiency)
    if is_admin:
        return True, "Admin access granted"
    
    # Check if owner
    if user['id'] == resource['owner_id']:
        return True, "Owner access granted"
    
    # Check if public resource and user is verified
    if resource['is_public'] and user['is_verified']:
        return True, "Public access granted to verified user"
    
    # No access conditions met
    return False, "Access denied"

def get_user_by_id(user_id):
    # Simulate database lookup
    users = {
        101: {'id': 101, 'name': 'Alice', 'is_verified': True},
        102: {'id': 102, 'name': 'Bob', 'is_verified': False},
        103: {'id': 103, 'name': 'Charlie', 'is_verified': True}
    }
    return users.get(user_id)

def get_resource_by_id(resource_id):
    # Simulate database lookup
    resources = {
        1: {'id': 1, 'name': 'Company Wiki', 'owner_id': 101, 'is_public': True},
        2: {'id': 2, 'name': 'Financial Report', 'owner_id': 101, 'is_public': False},
        3: {'id': 3, 'name': 'Project Plan', 'owner_id': 102, 'is_public': False}
    }
    return resources.get(resource_id)

# Test the function
test_cases = [
    (101, 1, False),  # Alice accessing her public Wiki
    (102, 1, False),  # Bob accessing public Wiki (but he's not verified)
    (103, 1, False),  # Charlie accessing public Wiki (verified user)
    (102, 2, False),  # Bob accessing Alice's private Financial Report
    (101, 3, False),  # Alice accessing Bob's private Project Plan
    (103, 3, True)    # Charlie as admin accessing Bob's Project Plan
]

for user_id, resource_id, is_admin in test_cases:
    user = get_user_by_id(user_id)
    resource = get_resource_by_id(resource_id)
    has_access, reason = check_access(user_id, resource_id, is_admin)
    
    print(f"User: {user['name']}, Resource: {resource['name']}")
    print(f"Access: {'✅' if has_access else '❌'} - {reason}\n")

This example demonstrates how logical operators can implement complex business rules for an access control system, combining multiple conditions with appropriate short-circuiting for efficiency.

Common Patterns and Techniques

Let's explore some common patterns and techniques using comparison and logical operators.

Chained Comparisons

Python allows you to chain multiple comparisons, making your code more readable:

# Traditional way with logical operators
if x > 5 and x < 10:
    print("x is between 5 and 10")

# More readable with chained comparison
if 5 < x < 10:
    print("x is between 5 and 10")

# More complex chains
if 0 <= x <= 100 and 0 <= y <= 100:
    print("Both x and y are in the range [0, 100]")

age = 25
if 18 <= age < 65:
    print("Working age adult")

Each part of a chained comparison is evaluated separately, and they're combined with and operators behind the scenes.

Conditional Assignment (Ternary Operator)

# Traditional if-else
if condition:
    x = value1
else:
    x = value2

# Ternary operator equivalent
x = value1 if condition else value2

# Examples
age = 20
status = "adult" if age >= 18 else "minor"
print(status)  # "adult"

# Can be chained for multiple conditions
category = "child" if age < 13 else "teenager" if age < 18 else "adult"
print(category)  # "adult"

The ternary operator is concise, but be careful not to sacrifice readability with overly complex expressions.

Using any() and all() Functions

Python provides two built-in functions that work with logical operations across iterables:

# any() returns True if at least one item is True
print(any([False, False, True, False]))  # True
print(any([]))  # False (empty iterable)

# all() returns True if all items are True
print(all([True, True, True]))  # True
print(all([True, False, True]))  # False
print(all([]))  # True (empty iterable)

# Practical examples
def has_uppercase(password):
    """Check if password has at least one uppercase letter."""
    return any(char.isupper() for char in password)

def all_digits(code):
    """Check if all characters in code are digits."""
    return all(char.isdigit() for char in code)

print(has_uppercase("Hello123"))  # True
print(has_uppercase("hello123"))  # False
print(all_digits("12345"))  # True
print(all_digits("123a45"))  # False

Handling None Values Safely

# Risky - might raise AttributeError if value is None
def get_user_name(user):
    return user.name

# Safer with explicit None check
def get_user_name_safe(user):
    if user is not None:
        return user.name
    return "Guest"

# Pythonic approach with short-circuit evaluation
def get_user_name_pythonic(user):
    return user and user.name or "Guest"

# Alternative with ternary operator
def get_user_name_ternary(user):
    return user.name if user else "Guest"

Checking Membership with in and not in

# Check if an item is in a collection
fruits = ["apple", "banana", "cherry"]
if "apple" in fruits:
    print("We have apples!")

# Check if an item is not in a collection
if "orange" not in fruits:
    print("We don't have oranges!")

# Check if substring is in a string
message = "Hello, world!"
if "world" in message:
    print("Found 'world' in the message!")

# Check if a key is in a dictionary
user = {"name": "Alice", "age": 30}
if "email" not in user:
    print("Email address missing!")

Real-World Example: Data Processing Pipeline

def process_data_entry(entry, required_fields, optional_fields):
    """
    Process a data entry, validating and normalizing fields.
    Returns a tuple of (success, processed_entry, error_message)
    """
    processed = {}
    
    # Check for missing required fields
    missing_fields = [field for field in required_fields if field not in entry]
    if missing_fields:
        return (False, None, f"Missing required fields: {', '.join(missing_fields)}")
    
    # Process all available fields (required + optional that are present)
    for field in required_fields + [f for f in optional_fields if f in entry]:
        value = entry[field]
        
        # Skip empty optional fields
        if field in optional_fields and (value is None or value == ""):
            continue
            
        # Validate and normalize based on field type
        if field == "email":
            if "@" not in value or "." not in value:
                return (False, None, f"Invalid email format: {value}")
            processed[field] = value.lower().strip()
            
        elif field == "age":
            try:
                age = int(value)
                if not (0 <= age <= 120):
                    return (False, None, f"Age out of valid range: {age}")
                processed[field] = age
            except ValueError:
                return (False, None, f"Age must be a number: {value}")
                
        elif field == "country":
            normalized = value.strip().upper()
            if len(normalized) != 2 or not all(c.isalpha() for c in normalized):
                return (False, None, f"Country code must be a 2-letter code: {value}")
            processed[field] = normalized
            
        else:
            # Default processing for other fields
            if isinstance(value, str):
                processed[field] = value.strip()
            else:
                processed[field] = value
    
    # Add derived fields
    if "first_name" in processed and "last_name" in processed:
        processed["full_name"] = f"{processed['first_name']} {processed['last_name']}"
    
    return (True, processed, "Success")

# Example usage
data_entries = [
    {
        "first_name": "Alice", 
        "last_name": "Smith",
        "email": "alice@example.com",
        "age": "28",
        "country": "us"
    },
    {
        "first_name": "Bob",
        "email": "not-an-email",
        "age": "thirty",
        "country": "usa"
    },
    {
        "first_name": "Charlie",
        "last_name": "Brown",
        "email": "charlie@example.com"
        # Missing age and country
    }
]

required_fields = ["first_name", "email"]
optional_fields = ["last_name", "age", "country"]

for i, entry in enumerate(data_entries):
    print(f"\nProcessing entry {i+1}:")
    success, processed, message = process_data_entry(entry, required_fields, optional_fields)
    
    if success:
        print("✅ Successfully processed:")
        for key, value in processed.items():
            print(f"  {key}: {value}")
    else:
        print(f"❌ Error: {message}")

This example demonstrates how comparison and logical operators can be combined to create a sophisticated data validation and processing pipeline, handling various edge cases and providing appropriate error messages.

Operator Precedence

When multiple operators appear in an expression, their precedence determines the order of evaluation. Here's the precedence of the operators we've discussed, from highest to lowest:

  1. Parentheses ()
  2. Comparison operators ==, !=, >, >=, <, <=, is, is not, in, not in
  3. not
  4. and
  5. or
# Precedence examples
print(True or False and False)  # True (and has higher precedence than or)
# Equivalent to: True or (False and False) = True or False = True

print(not True or False)  # False (not has higher precedence than or)
# Equivalent to: (not True) or False = False or False = False

# Using parentheses to override precedence
print((True or False) and False)  # False
print(not (True or False))  # False

When in doubt about operator precedence, use parentheses to make your intentions explicit. This not only ensures the correct evaluation order but also improves code readability.

Confused Precedence: Common Mistakes

# Common mistake: forgetting that 'not' has higher precedence than 'and'/'or'
x = 5
print(not x > 3 and x < 10)  # False
# Interpreted as: (not (x > 3)) and (x < 10) = False and True = False

# Correct version with parentheses
print(not (x > 3 and x < 10))  # False

# Another common mistake: chaining comparisons incorrectly
y = 15
print(5 < x < 10 < y)  # False (correct: compares each pair)
print(5 < x and x < 10 and 10 < y)  # False (equivalent to above)

# But this is different (and probably not what was intended):
print(5 < x < 10 and 10 < y)  # False
# Interpreted as: (5 < x < 10) and (10 < y) = False and True = False

Remember that Python's comparison operators chain in a way that's different from most other programming languages. Each part of a chained comparison is evaluated separately and combined with logical AND.

Best Practices and Common Pitfalls

Best Practices

Common Pitfalls

Practice Exercises

To solidify your understanding of comparison and logical operators, try these exercises. Solutions will be reviewed in class.

  1. Basic Comparison: Write a function that takes two numbers and returns the larger one. If they are equal, return "Equal".

  2. Categorize BMI: Write a function that calculates BMI (weight in kg / height in meters squared) and returns a category: "Underweight" (< 18.5), "Normal" (18.5-24.9), "Overweight" (25-29.9), or "Obese" (≥ 30).

  3. Logical AND, OR: Write a function that determines if a year is a leap year. A leap year is divisible by 4 AND (not divisible by 100 OR divisible by 400).

  4. Short-Circuit Evaluation: Write a function called safe_divide(a, b) that uses short-circuit evaluation to avoid division by zero. It should return None if b is 0.

  5. Student Grade Classifier: Write a function that takes a student's score and classifies it as "A" (90-100), "B" (80-89), "C" (70-79), "D" (60-69), or "F" (below 60).

  6. Password Strength Checker: Write a function that checks if a password is strong. A strong password:

    • Is at least 8 characters long
    • Contains at least one uppercase letter
    • Contains at least one lowercase letter
    • Contains at least one digit
    • Contains at least one special character from !@#$%^&*()

  7. Advanced Challenge: Write a function that determines if a triangle is valid based on three side lengths. A triangle is valid if the sum of any two sides is greater than the third side (this must be true for all three combinations).

Real-World Applications

Comparison and logical operators are foundational to programming logic and are used extensively in real-world applications:

Example: E-commerce Product Filter

def filter_products(products, filters):
    """
    Filter a list of products based on user-defined filters.
    
    Parameters:
    - products: list of dictionaries containing product information
    - filters: dictionary with filter criteria
    
    Returns:
    - Filtered list of products
    """
    filtered_products = []
    
    for product in products:
        # Start with the assumption that the product matches all filters
        matches_all_filters = True
        
        # Price range filter
        if "min_price" in filters and product["price"] < filters["min_price"]:
            matches_all_filters = False
            
        if "max_price" in filters and product["price"] > filters["max_price"]:
            matches_all_filters = False
            
        # Category filter
        if "categories" in filters and product["category"] not in filters["categories"]:
            matches_all_filters = False
            
        # Brand filter
        if "brands" in filters and product["brand"] not in filters["brands"]:
            matches_all_filters = False
            
        # Rating filter (minimum rating)
        if "min_rating" in filters and product["rating"] < filters["min_rating"]:
            matches_all_filters = False
            
        # Availability filter
        if "in_stock_only" in filters and filters["in_stock_only"] and product["stock"] <= 0:
            matches_all_filters = False
            
        # Special features filter (all selected features must be present)
        if "features" in filters:
            for feature in filters["features"]:
                if feature not in product["features"]:
                    matches_all_filters = False
                    break
        
        # Text search (search in name and description)
        if "search_text" in filters and filters["search_text"]:
            search_text = filters["search_text"].lower()
            product_text = (product["name"] + " " + product["description"]).lower()
            if search_text not in product_text:
                matches_all_filters = False
        
        # If the product matched all filters, add it to the results
        if matches_all_filters:
            filtered_products.append(product)
    
    # Sort results based on sort criteria
    if "sort_by" in filters:
        if filters["sort_by"] == "price_low_high":
            filtered_products.sort(key=lambda p: p["price"])
        elif filters["sort_by"] == "price_high_low":
            filtered_products.sort(key=lambda p: p["price"], reverse=True)
        elif filters["sort_by"] == "rating":
            filtered_products.sort(key=lambda p: p["rating"], reverse=True)
        elif filters["sort_by"] == "popularity":
            filtered_products.sort(key=lambda p: p["sales_count"], reverse=True)
    
    return filtered_products

# Example usage
products = [
    {
        "id": 1,
        "name": "Laptop Pro X",
        "category": "electronics",
        "brand": "TechMaster",
        "price": 1299.99,
        "rating": 4.5,
        "stock": 10,
        "features": ["SSD", "16GB RAM", "4K Display"],
        "description": "Powerful laptop for professionals and gamers.",
        "sales_count": 253
    },
    {
        "id": 2,
        "name": "Smartphone Y12",
        "category": "electronics",
        "brand": "Globex",
        "price": 799.99,
        "rating": 4.2,
        "stock": 25,
        "features": ["5G", "Dual Camera", "Face Recognition"],
        "description": "Latest smartphone with cutting-edge features.",
        "sales_count": 412
    },
    {
        "id": 3,
        "name": "Coffee Maker Deluxe",
        "category": "appliances",
        "brand": "HomeStyle",
        "price": 89.99,
        "rating": 4.0,
        "stock": 0,
        "features": ["Programmable", "Self-cleaning", "Thermal Carafe"],
        "description": "Start your day with perfect coffee every time.",
        "sales_count": 128
    }
]

# User-selected filters
user_filters = {
    "min_price": 100,
    "max_price": 1500,
    "categories": ["electronics"],
    "min_rating": 4.0,
    "in_stock_only": True,
    "sort_by": "rating"
}

filtered_results = filter_products(products, user_filters)

# Display results
print(f"Found {len(filtered_results)} products matching your filters:")
for product in filtered_results:
    print(f"- {product['name']} (${product['price']:.2f}) - {product['rating']} stars")

This example demonstrates how comparison and logical operators are combined to implement a sophisticated product filtering system similar to what you'd find on major e-commerce websites. Multiple filter criteria are applied, with each one potentially eliminating products that don't match.

Conclusion and Next Steps

We've explored comparison and logical operators in Python, covering their syntax, behavior, and common patterns. These operators form the foundation of decision-making in your code, allowing you to create dynamic, responsive programs that can adapt to different conditions.

As you continue your Python journey, you'll find that these operators appear in virtually every aspect of programming, from simple conditional statements to complex algorithms and business logic. Mastering them will significantly enhance your ability to write effective code.

Next, we'll build on these concepts as we explore control flow structures like if statements, loops, and functions, which rely heavily on comparison and logical operations to determine execution paths.

Remember to practice these concepts by completing the exercises and experimenting with your own examples. The ability to craft precise logical expressions is one of the most important skills you'll develop as a programmer.

For further exploration, check out the official Python documentation: https://docs.python.org/3/reference/expressions.html#comparisons