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 |
|---|---|---|---|
| True only if both operands are true |
|
|
| True if at least one operand is true |
|
|
| Inverts a boolean value |
|
|
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
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
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
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 |
| Tightest — applies to the single value right after it |
Middle |
| Groups before |
Lowest |
| Loosest — evaluated last |
not binds tighter than and, which binds tighter than or
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
The bitwise precedence trap
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