Boolean Operators in Python: Complete and, or, not Logical Operations and Truth Value Testing Guide

Boolean Operators in python

Boolean Operators in python

I remember the exact moment boolean logic clicked for me in Python — I was writing a login validator with three or four conditions chained together, and I realized I didn’t actually understand what and and or were doing behind the scenes beyond “true or false stuff.” Once I dug into it properly, I found that Python’s boolean operators are more nuanced and more powerful than the simple true/false gates I originally assumed. This guide covers everything I’ve learned, from the absolute basics to the internal mechanics.

The Three Boolean Operators

Python has exactly three boolean (logical) operators: and, or, and not. Unlike many languages, these are actual keywords, not symbols like &&, ||, or !.

a = True
b = False

print(a and b)  # False
print(a or b)   # True
print(not a)    # False

That’s the surface-level behavior everyone learns first. But the real story starts with what these operators actually return.

Boolean Operators Don’t Just Return True or False

This is the single most important thing I wish someone had told me earlier: and and or in Python don’t always return True or False — they return one of their actual operands.

print(5 and 10)     # Output: 10
print(0 and 10)      # Output: 0
print(5 or 10)        # Output: 5
print(0 or 10)         # Output: 10
print("" or "default")  # Output: "default"

The rule is:

This is why the common Python idiom for default values works:

name = user_input or "Anonymous"

If user_input is an empty string (falsy), name becomes "Anonymous". If it has content (truthy), name becomes user_input.

Truth Value Testing

Every object in Python has an inherent truth value, used implicitly whenever it appears in a boolean context (an if statement, a while loop, or as an operand to and/or/not).

By default, an object is considered truthy unless its class defines a __bool__() method that returns False, or a __len__() method that returns 0.

Objects considered falsy by default:

falsy_values = [False, None, 0, 0.0, 0j, "", (), [], {}, set(), range(0)]
for val in falsy_values:
    print(val, "->", bool(val))

Everything else is truthy, including non-empty strings, non-zero numbers, and non-empty containers.

print(bool("hello"))   # True
print(bool([1, 2, 3])) # True
print(bool(-1))        # True, even negative numbers are truthy

Custom Truth Values

I can define how my own classes behave in boolean contexts by implementing __bool__:

class Cart:
    def __init__(self, items):
        self.items = items

    def __bool__(self):
        return len(self.items) > 0

empty_cart = Cart([])
full_cart = Cart(["apple", "banana"])

print(bool(empty_cart))  # False
print(bool(full_cart))   # True

if full_cart:
    print("Cart has items")

If __bool__ isn’t defined, Python falls back to __len__, and if neither is defined, the object is always truthy.

Short-Circuit Evaluation

Python’s and and or are short-circuiting, meaning evaluation stops as soon as the result is determined. This isn’t just an optimization detail — it changes program behavior when operands have side effects.

def log_and_return(value, label):
    print(f"Evaluating {label}")
    return value

result = log_and_return(False, "first") and log_and_return(True, "second")
print(result)

Output:

Evaluating first
False

Notice "second" never gets evaluated because False and anything is always False. I use this pattern constantly to guard against errors:

data = None

if data and data.get("key"):
    print("Found key")
else:
    print("No data or key missing")

If data is None, Python never attempts data.get("key"), avoiding an AttributeError.

The not Operator

not always returns a genuine boolean (True or False), unlike and/or.

print(not 0)         # True
print(not "hello")   # False
print(not [])        # True
print(not not 5)     # True (double negation coerces to real bool)

A common idiom I use is not not x (or the cleaner bool(x)) when I specifically need a real boolean rather than a truthy/falsy value.

Chaining Boolean Logic

Boolean operators chain naturally, and precedence (not > and > or) determines grouping:

age = 25
has_id = True
is_banned = False

can_enter = age >= 18 and has_id and not is_banned
print(can_enter)  # True

For anything beyond two or three conditions, I break it into named variables — it reads far better and is easier to debug:

is_adult = age >= 18
has_valid_id = has_id
not_banned = not is_banned

can_enter = is_adult and has_valid_id and not_banned

How Boolean Evaluation Works Internally

At the bytecode level, CPython compiles and/or into conditional jump instructions rather than function calls, which is part of why short-circuiting is efficient — there’s no wasted evaluation.

import dis

def check(a, b):
    return a and b

dis.dis(check)

The disassembly shows a JUMP_IF_FALSE_OR_POP-style instruction (naming varies by Python version), confirming that the second operand is only evaluated if the first doesn’t already determine the result. This is a genuine control-flow branch, not a two-argument function call — which is also why and/or can’t be overridden the way __add__ or __eq__ can, since they’re baked into the grammar itself.

Boolean Operators vs Bitwise Operators

A mistake I made early on was confusing and/or with &/|. They’re not interchangeable:

print(True and False)  # False - boolean logic
print(True & False)    # False - bitwise, but on bools, coincides

print(5 and 3)   # 3 - short-circuit logic
print(5 & 3)     # 1 - bitwise AND on the actual bits

For anything with pandas or NumPy, this distinction is critical, since and/or don’t work element-wise on arrays — you’re forced to use &, |, and ~ there instead, with parentheses around each condition because bitwise operators have higher precedence than comparisons.

Real-World Use Cases

Input validation:

def is_valid_password(password):
    return (
        len(password) >= 8
        and any(c.isupper() for c in password)
        and any(c.isdigit() for c in password)
    )

print(is_valid_password("Secure123"))  # True

Default value chains:

config_value = user_override or environment_variable or default_setting

Guarding against None before attribute access:

user = get_user()
username = user and user.name

Feature flags and conditional automation:

should_run = is_enabled and not is_maintenance_mode
if should_run:
    run_pipeline()

Best Practices

Common Mistakes

# Mistake: expecting element-wise behavior
import numpy as np
arr = np.array([1, 2, 3])
# if arr and True:  # Raises ValueError: truth value of array ambiguous

# Mistake: assuming 'and'/'or' always return True/False
x = 0 or []
print(x)  # [], not False — easy to misuse in conditions expecting a strict bool

Debugging Tips

When boolean logic misbehaves, I isolate each sub-expression:

print("cond1:", condition1)
print("cond2:", condition2)
print("combined:", condition1 and condition2)

I also double check truthiness explicitly when unsure:

print(bool(some_value))

FAQs

Can I overload and/or for custom classes like I can with + or ==? No — and and or are keywords tied to control flow, not operator-overloadable dunder methods. What I can customize is truthiness itself, via __bool__.

Is 0.0 and 5 the same as False and 5? Functionally yes for the branching decision — 0.0 is falsy — but the returned value is 0.0, not False.

Why does not always return a real boolean while and/or don’t? not is defined specifically to produce a genuine boolean result, unlike and/or, which are designed to return one of the operands for flexibility in default-value patterns.

Summary

Boolean operators in Python go well beyond simple true/false gates. and and or return actual operand values rather than strict booleans, not always yields a genuine boolean, truthiness is customizable per class via __bool__, and short-circuit evaluation isn’t just an optimization — it’s a control-flow tool I rely on daily to write safer, cleaner conditional code.

References

Exit mobile version