PythonLogical Operators

Logical Operators

Logical operators combine or invert boolean expressions. Python spells them out as words — and, or, and not — rather than symbols like &&, ||, and ! used in C-family languages. They're used constantly in if conditions, while loops, and to build compound boolean expressions, but they also have a second, very idiomatic use in Python: picking a default value or short-circuiting a function call.

The three logical operators

Operator

Meaning

Example

Result

and

True only if both operands are true

True and False

False

or

True if at least one operand is true

True or False

True

not

Inverts a boolean value

not True

False

Short-circuit evaluation

Both and and or are short-circuiting: they stop evaluating as soon as the overall result is already determined, and they don't bother evaluating the right-hand side at all if it isn't needed. For and, if the left operand is falsy, the result is already False (or that falsy value), so the right side is skipped. For or, if the left operand is truthy, the result is already determined, so the right side is skipped.

This isn't just an optimization detail — it's something you can rely on for control flow, and it means a function call on the right-hand side might never actually run.

Proving the right-hand side never executes

Python
def expensive_check():
    print("expensive_check() was called!")
    return True

# 'and' short-circuits on a falsy left side -- expensive_check() never runs:
result = False and expensive_check()
print("Result:", result)

print("---")

# 'or' short-circuits on a truthy left side -- expensive_check() never runs:
result = True or expensive_check()
print("Result:", result)
Result: False
---
Result: True

Notice that "expensive_check() was called!" never printed in either case — proof that the right-hand side genuinely wasn't evaluated, not just that its return value was ignored. This is why short-circuiting is safe to use as a guard, e.g. user is not None and user.is_active won't raise an AttributeError even if user is None, because user.is_active never runs when the left side is falsy.

The `or` default-value idiom

Because or returns whichever operand actually determined the result (not just True/False), it's commonly used to supply a fallback value when something might be empty, zero, or None.

Using `or` to pick a default

Python
username = ""
display_name = username or "Guest"
print(display_name)   # "Guest" -- empty string is falsy, so we fall through

config_timeout = 0
timeout = config_timeout or 30
print(timeout)   # 30 -- 0 is falsy too, careful with this if 0 is meaningful!

name = "Ada"
display_name = name or "Guest"
print(display_name)   # "Ada" -- name is truthy, so it's returned as-is
`or` treats every falsy value as “missing”
`0`, `""`, `[]`, `{}`, and `None` are all falsy in Python. The `x or default` idiom can't tell the difference between "genuinely unset" and "legitimately zero/empty." If zero or an empty string is a valid, meaningful value in your program, use an explicit check instead: `x if x is not None else default`.
Operator Precedence: not, and, or

When you mix all three in one expression without parentheses, Python evaluates not first, then and, then or — from tightest-binding to loosest. This mirrors how most people intuitively read boolean logic, but it's worth internalizing so you don't have to guess.

Precedence

Operator

Binds

Highest

not

Tightest — applies to the single value right after it

Middle

and

Groups before or

Lowest

or

Loosest — evaluated last

not binds tighter than and, which binds tighter than or

Python
a, b, c = True, False, True

print(not a and b or c)
# Reads as: ((not a) and b) or c
# = (False and False) or True
# = False or True
# = True

print(a or b and not c)
# Reads as: a or (b and (not c))
# = True or (False and False)
# = True or False
# = True
True
True
Don't reach for `& | ~` on booleans
Python also has bitwise operators `&` (AND), `|` (OR), and `~` (NOT) — but these operate on the binary representation of integers, not on boolean logic, and they do **not** short-circuit. Using them on booleans often happens to work by coincidence (because `True`/`False` behave like `1`/`0`), which makes the mistake easy to miss until it silently breaks. Worse, `&` and `|` have *higher* precedence than comparison operators, which creates a classic trap:

The bitwise precedence trap

Python
x = 5

# What you probably meant: "is x greater than 0 AND less than 10?"
print(x > 0 and x < 10)     # True -- correct, as expected

# The bitwise version -- looks similar, but binds differently:
print(x > 0 & x < 10)
# '&' binds tighter than '>' and '<', so this is actually:
# x > (0 & x) < 10
# = x > 0 < 10          (0 & x is 0)
# = (x > 0) and (0 < 10)  -- happens to still be True here, but for the WRONG reason

y = 0
print(y > 0 and y < 10)     # False -- correctly rejects y = 0
print(y > 0 & y < 10)       # Also False here, but the reasoning is completely different
                            # and this equivalence breaks down for other values
Rule of thumb
Use `and`, `or`, and `not` for boolean logic on `True`/`False` values and conditions. Reserve `&`, `|`, `~`, and `^` for actual bit manipulation on integers (flags, masks) or for element-wise logic on NumPy/pandas boolean arrays, where the bitwise operators are used deliberately because Python's `and`/`or` can't be overloaded to work element-wise.