Boolean operators and truthiness¶
In this tutorial, you will go beyond if/elif/else to understand how Python evaluates boolean expressions, what counts as truthy or falsy, and how to use these properties idiomatically.
Time commitment: 15–20 minutes
Prerequisites:
- Completion of If statements
- Comfort with Python variables, strings, numbers, and lists
Learning objectives¶
By the end of this tutorial, you will be able to:
- Use
and,or, andnotand explain short-circuit evaluation - Explain what
andandoractually return (it isn't alwaysTrueorFalse) - Identify Python's falsy values
- Choose between idiomatic truthiness checks and explicit comparisons
A quick recap¶
You met and, or, and not in the previous tutorial. They combine boolean expressions:
andis true when both sides are trueoris true when at least one side is truenotflips a boolean to its opposite
is_member = True
has_paid = False
print(is_member and has_paid) # False
print(is_member or has_paid) # True
print(not has_paid) # True
That much was the headline. The interesting story is in the details — particularly how and when Python evaluates each side of an expression.
Short-circuit evaluation¶
Python evaluates and and or left to right and stops as soon as the answer is decided. This is called short-circuit evaluation.
For and, as soon as Python finds a falsy value, it knows the whole expression is false — there is no point checking the rest.
For or, as soon as Python finds a truthy value, the whole expression is true — again, no need to check the rest.
def expensive_check():
print("expensive_check() ran")
return True
# expensive_check is never called — `False and ...` is already False
print(False and expensive_check())
print()
# expensive_check is never called — `True or ...` is already True
print(True or expensive_check())
This matters in two practical ways. First, you can avoid wasted work — put the cheap check first. Second, you can use it as a guard against errors:
user = None
# Without short-circuiting, `user.name` would raise AttributeError
# With short-circuiting, Python stops at `user` (which is falsy) and returns it
display_name = user and user.name
print(display_name) # None
and and or return one of their operands¶
This is the part that surprises most people: and and or do not return True or False. They return whichever operand decided the outcome.
For or, that's the first truthy value (or the last value, if all are falsy).
For and, it's the first falsy value (or the last value, if all are truthy).
print("" or "default") # "default" — first was falsy, returned the second
print("hello" or "default") # "hello" — first was truthy, returned it
print(0 and 1) # 0 — first was falsy, returned it
print(1 and 2) # 2 — first was truthy, returned the second
When an if statement uses these expressions, Python coerces the result to a boolean — but the underlying value is what was returned.
This behaviour is the foundation of a very common idiom:
raw_input = "" # imagine this came from a form field
name = raw_input or "Anonymous"
print(name) # "Anonymous" — empty string was falsy, fell through to default
Truthiness: what counts as true?¶
Python lets you put almost anything in an if condition. Behind the scenes, it converts the value to a boolean using a small set of rules.
The falsy values in Python are:
FalseNone- Numeric zero:
0,0.0,0j - Empty sequences:
"",(),[] - Empty mappings and sets:
{},set() - Empty bytes:
b""
Everything else is truthy. That's the whole list.
Try a few in the cell below and see for yourself:
for value in [0, 1, -1, "", "hello", [], [0], None, False, True, 0.0, 0.1]:
if value:
print(f"{value!r:10} -> truthy")
else:
print(f"{value!r:10} -> falsy")
Idiomatic truthiness checks¶
Python programmers use truthiness all the time. It makes conditions concise and reads almost like English.
if items: # "if there are any items..."
if not name: # "if name is missing or empty..."
if errors: # "if any errors were collected..."
Compare those to the explicit forms:
if len(items) > 0:
if name == "" or name is None:
if len(errors) > 0:
Both are valid. The truthiness form is shorter and more idiomatic; the explicit form makes the type assumption visible. Most working Python uses the truthiness form.
# Truthiness in action — a function that handles a possibly-empty list
def announce(items):
if items:
print(f"Today's specials: {', '.join(items)}")
else:
print("No specials today.")
announce(["soup", "salad"])
announce([])
When to be explicit¶
Truthiness collapses a lot of distinctions. 0, None, "", and [] all look the same to if. When you need to distinguish "missing" from "zero" or "empty", reach for an explicit is None check:
def record_score(name, score=None):
if score:
print(f"{name} scored {score}")
else:
print(f"No score recorded for {name}")
# This is fine for most scores...
record_score("Ada", 92)
# ...but a score of 0 is treated as missing!
record_score("Ben", 0)
The fix is to ask the right question. We don't actually want "is score truthy?" — we want "did the caller supply a value?":
def record_score(name, score=None):
if score is not None: # the right check
print(f"{name} scored {score}")
else:
print(f"No score recorded for {name}")
record_score("Ada", 92)
record_score("Ben", 0)
record_score("Cas")
Rule of thumb:
- Use truthiness (
if items:,if not name:) when "empty" and "missing" mean the same thing in this context. - Use
is None/is not Nonewhen the difference between zero/empty and missing matters.
Customising truthiness¶
When you write your own classes, you can decide what "truthy" means for instances by defining __bool__. If you don't, every instance is truthy by default.
class Reading:
def __init__(self, value, valid):
self.value = value
self.valid = valid
def __bool__(self):
return self.valid
good = Reading(value=21.4, valid=True)
bad = Reading(value=99.9, valid=False)
if good:
print(f"Got reading: {good.value}")
if not bad:
print("Skipped invalid reading.")
If your class is container-like and defines __len__, Python uses that automatically — empty means falsy.
class Queue:
def __init__(self):
self._items = []
def __len__(self):
return len(self._items)
q = Queue()
if not q:
print("queue is empty") # this prints
For more on this protocol, see the Truthiness rules reference and the Why truthiness works the way it does concepts essay.
Exercise: a sensible default¶
Write a function greet(name=None) that prints a friendly greeting. The rules:
- If
nameisNone, print"Hello, friend!" - If
nameis an empty string, also print"Hello, friend!" - If
nameis any other string, print"Hello, <name>!"
Hint: Both None and "" are falsy, so a single truthiness check covers both cases.
# Write your greet function here
def greet(name=None):
pass
# Tests
greet() # Expected: Hello, friend!
greet("") # Expected: Hello, friend!
greet("Ada") # Expected: Hello, Ada!
Solution¶
A truthiness check on name does the job in one branch:
def greet(name=None):
if name:
print(f"Hello, {name}!")
else:
print("Hello, friend!")
greet()
greet("")
greet("Ada")
You could also write it using the or idiom you saw earlier:
def greet(name=None):
print(f"Hello, {name or 'friend'}!")
Both are idiomatic. The or form is shorter; the if form is easier to extend if you later add more rules.
Summary¶
In this tutorial, you learned:
- How short-circuit evaluation lets
andandorskip the right-hand side when the result is already known - That
andandorreturn one of their operands, not necessarilyTrueorFalse— and how thevalue or defaultidiom uses this - The full list of falsy values in Python:
False,None, numeric zero, and empty containers - When to use truthiness (
if items:) and when to be explicit (if items is not None:) - How to make your own classes participate in truthiness with
__bool__
What is next¶
The final tutorial in this guide introduces a different approach to branching:
- Pattern matching with
match/case— using structural patterns whenif/elifwould be repetitive.