Booleans and Comparisons
Every decision a program makes comes down to one of two values. Comparisons produce them, and and/or/not combine them.
Overview
== compares value, is compares identity
== asks "are these equal?" is asks "are these the same object?"
a = [1, 2, 3]
b = [1, 2, 3]
c = a
a == b # True - same contents
a is b # False - two separate lists
a is c # True - one list, two namesUse is only for None, True and False, which are singletons — there is exactly one None object in a running program.
if result is None: # correct
if name == "Ada": # correct
if count is 100: # wrong, and modern Python warns about itThe trap is that small integers and short strings are cached and reused by the interpreter, so x is 100 sometimes appears to work — and then fails silently for 1000, in production, months later.
A comparison is an expression with a value
== asks a question. = assigns. Mixing them up is the single most common beginner typo.
Combining and negating
and needs both sides true, or needs either, and not flips.
What counts as false
== compares. = assigns. Python raises a SyntaxError if you use = in an if, which is a kindness.18 < age < 65 is one expression, and means what it looks like.False, 0, "", [], {}, None. Everything else is truthy.if items: is the idiomatic way to ask "is this list non-empty?" - no length comparison needed.Booleans and Comparisons: A Practical Guide
Comparisons produce True or False, and combining them with and, or and not is how a program makes decisions. The details that catch people are short-circuiting and the difference between == and is.
True, False, and everything that behaves like them
A boolean has two values, True and False, capitalised. They are what every comparison produces and what every if consumes.
5 > 3 # True
5 == 3 # False
type(True) # <class 'bool'>
True + True # 2 -- booleans are integers underneath
sum([True, False, True]) # 2 -- which makes counting matches easyThat last line is a genuinely useful idiom: sum(x > 100 for x in values) counts how many values exceed 100, because each comparison contributes 1 or 0.
The comparison operators:
a == b # equal in value
a != b # not equal
a < b # less than (also <=, >, >=)
a is b # the same object in memory
a in items # membershipComparisons chain, exactly as in mathematics, and each operand is evaluated once:
if 0 <= score <= 100: # clearer than score >= 0 and score <= 100
and, or, not, and short-circuiting
True and False # False - both must be true
True or False # True - either will do
not True # FalseThe important behaviour is short-circuiting: and stops at the first false operand, or at the first true one, and the rest is never evaluated. That is not an optimisation detail; it is how you write safe checks.
if user is not None and user.is_admin: # user.is_admin never runs
... # when user is NoneReverse the order and it raises AttributeError on exactly the case the check was protecting against.
Short-circuiting also means and and or return one of their operands, not a boolean:
name = given_name or "Anonymous" # falls back when given_name is empty
port = config.get("port") or 8080 # careful: 0 is falsy, so 0 becomes 8080That second line shows the one caveat — when 0 or "" are legitimate values, use an explicit is None test instead.
Truthiness
Every object can be used as a condition. These are false:
False None 0 0.0 "" [] {} () set()
Everything else is true, including "False", [0] and -1.
if items: # idiomatic "is this non-empty"
if not items: # "is this empty"Two useful builtins that fold a whole sequence into one boolean:
any(x > 100 for x in values) # True if at least one qualifies
all(x > 0 for x in values) # True if every one doesBoth short-circuit, so any() stops at the first match — which makes them efficient as well as readable.
Guided experiments
- See short-circuiting. Write False and print("ran") and run it. Nothing prints. Change the first operand to True and the print executes.
- Guard an index. Try items and items[0] with an empty list, then flip the operands and watch it raise IndexError.
- Compare identity and value. Build two lists with the same contents and test both == and is. Same value, different objects.
- Chain a comparison. Run 1 < 5 < 10 and then 10 < 5 < 1 to confirm both bounds are actually being checked.
What trips people up
- Using is to compare values. Works by accident for small integers and cached strings, then fails on the first value outside the cache.
- Putting the guard on the wrong side. Short-circuiting only protects what comes after it.
- Comparing floats with ==. 0.1 + 0.2 == 0.3 is False, because binary floating point cannot represent those values exactly. Use math.isclose.
- Writing if x == True. Verbose and breaks for truthy-but-not-True values such as 1 or a non-empty list.
Key takeaway
Comparisons return booleans and can be chained the way they are written in mathematics. and and or short-circuit, which is what lets a guard on the left protect an expression on the right — so operand order matters. Use == for value and reserve is for None, True and False, and never compare floats for exact equality.
Comparing different types
1 == 1.0 # True - numeric types compare across the boundary
1 == True # True - because True IS 1
"1" == 1 # False - text is never equal to a number
[1, 2] == (1, 2) # False - a list is never equal to a tuple
"apple" < "banana" # True - alphabetical by character code
"Apple" < "apple" # True - uppercase sorts first in ASCII
[1, 2] < [1, 3] # True - element by elementStrings compare by character code, which is why "Zebra" < "apple" is true — all uppercase letters come before all lowercase ones. For human-friendly comparison, normalise the case first with .casefold().
Ordering comparisons between unrelated types raise TypeError in Python 3: 1 < "a" fails rather than producing an arbitrary answer. That is deliberate, and it catches a class of bug that Python 2 silently allowed.
Floats bring their own caveat: 0.1 + 0.2 == 0.3 is False. Compare with math.isclose(a, b) instead.
Writing conditions that read well
- Do not compare to
True.if is_ready:beatsif is_ready == True:, and both beatif is_ready is True:. - Name complicated conditions.
is_eligible = age >= 18 and has_id and not bannedthenif is_eligible:— the name is documentation and the condition becomes testable. - Prefer
inover chains ofor.if status in ("open", "pending", "review"):. - Use
not inrather thannot x in y, which parses correctly but reads badly. - Apply De Morgan's law when a negation gets tangled:
not (a and b)isnot a or not b, and one of the two is usually clearer for the case at hand.
Short-circuiting, made visible
The claim that the second operand is "never evaluated" is worth seeing rather than taking on trust:
def check(label, value):
print(" evaluated", label)
return value
print("A and B, with A false:")
print("result", check("A", False) and check("B", True))
print("A or B, with A true:")
print("result", check("A", True) or check("B", False))A and B, with A false:
evaluated A
result False
A or B, with A true:
evaluated A
result TrueB never prints in either case. and had its answer as soon as the left side was false, and or as soon as the left side was true, so neither looked further.
That is what makes if user is not None and user.active safe rather than merely lucky: the attribute access is not evaluated at all when the guard fails. It also means the right-hand side is the place for anything expensive or anything that might raise, and the left-hand side is the place for the cheap test that decides whether the expensive one is worth doing.
The corollary is that operand order carries meaning. Swapping the two halves of a guarded expression turns working code into an AttributeError on exactly the input the guard existed for, and nothing about the change looks dangerous.
Chained comparisons, exactly
0 <= score <= 100 is not two comparisons joined with and, and the difference shows when the middle term does something:
def side(n):
print(" evaluated", n)
return n
print(1 < side(5) < 10) evaluated 5
Trueside(5) runs once. Written as 1 < side(5) and side(5) < 10 it would run twice, which matters when the expression is expensive or has a side effect.
The rule is that a < b < c evaluates b once and is equivalent to a < b and b < c with b computed a single time — and it short-circuits in the same way, so c is never evaluated if the first comparison fails.
Any comparison operators can be chained, including == and in, which is occasionally confusing rather than useful: a == b == c tests that all three are equal, but a is b is c and mixtures like a < b == c are legal and rarely what anyone means at a glance. The idiomatic use is a range check, and that is where it should mostly stay.
Functions that return a boolean
A function whose job is to answer yes or no deserves a few specific habits, because these are the functions that end up inside every condition.
Name it as a question. is_valid, has_permission, can_retry, should_skip. A reader then knows what if user.can_retry(): means without looking, and the name reads correctly in both the positive and the negative form.
Return an actual boolean. return bool(matches) rather than return matches, when the caller only wants the answer. Returning the underlying list works because of truthiness and quietly commits you to that being a list forever.
**Do not return None for "no".** A function that returns True or None is usually a mistake rather than a design, and it makes the result unusable in anything that expects a boolean, such as sum() over conditions.
Avoid the negative name. is_not_ready produces if not is_not_ready, which nobody can read. Name the positive and let the caller negate it.
These matter more than they look because a boolean function is used inside conditions, where a misleading name causes a logic error rather than a crash — the kind that produces plausible wrong answers.
Where is genuinely belongs
The advice "use is only for None" is right and worth understanding rather than memorising, because the reason tells you the two other cases where it applies.
is asks whether two names refer to the same object. That question has a definite answer for singletons — objects the language guarantees there is exactly one of. None, True and False are the three, which is why x is None is both correct and the convention.
For everything else, identity is an implementation detail. CPython caches small integers and short strings, so x is 100 may be true and x is 1000 false, for the same code, with no visible difference. Relying on that is relying on an optimisation that is free to change, and modern Python emits a SyntaxWarning when it sees a literal on the right of is for exactly this reason.
The second honest use is a sentinel: an object created with object() purely so that arg is MISSING can distinguish "no argument supplied" from "supplied None". Identity is the whole point there, because the sentinel is deliberately equal to nothing else.
The third is genuine identity questions — "is this the same list the caller gave me, or a copy?" — which is what a is b was designed to answer and what id() reports.
Everywhere else, == is the operator, and defining __eq__ on your own classes is how you say what equality means for them.
Comparison in your own classes
By default, two instances of a class you wrote are equal only if they are the same object, because the inherited __eq__ compares identity. Two Point objects both holding (3, 4) are not equal, which surprises people the first time they compare results in a test.
Defining __eq__ fixes it, and brings one obligation with it: define __hash__ as well, or the class becomes unhashable and cannot go in a set or be a dictionary key. Python removes the inherited hash deliberately when you define equality, because a hash that disagreed with equality would break every dictionary the object went into. The two must agree — objects that compare equal must hash the same.
Ordering is separate again. < needs __lt__, and the other three operators are their own methods rather than being inferred, so an object with only __lt__ sorts correctly and raises on >=. functools.total_ordering fills the rest in, and @dataclass(order=True) writes all of them by comparing the fields in declaration order.
The shortcut worth knowing: a dataclass gives you __eq__ and, if you ask, ordering and hashing too, all consistent with each other. Writing them by hand is only worth it when the comparison should ignore some fields or normalise them first.
Questions people ask
Why is True == 1? Because bool is a subclass of int, with True equal to 1 and False to 0. It is what makes sum(conditions) count matches.
When should I use is? For None, True and False, and when you genuinely mean "the same object". Everywhere else, ==.
Why does 0.1 + 0.2 == 0.3 return False? Binary floating point cannot represent 0.1 exactly. Use math.isclose.
What does bool(x) do? Applies the truthiness rules and returns True or False — occasionally useful to normalise a value before storing it.
Can I use && and ||? No. Python uses the words and and or; & and | are bitwise operators and behave differently.
Is if x != None wrong? It usually works but is not None is the correct and conventional form, and it cannot be fooled by a class that overrides __eq__.
Why does [] == False return False? Because equality and truthiness are different questions. An empty list is falsy, and it is not equal to False. bool([]) == False is True.
Can I compare two dictionaries? With ==, yes — they are equal if they hold the same keys and values, regardless of insertion order. Ordering comparisons like < raise.
What does not not x do? The same as bool(x), more obscurely. Use bool(x).
Recap in one screen
- Comparisons produce booleans, and booleans are integers —
sum(...)over conditions counts them. ==compares value;iscompares identity, and is only forNone,TrueandFalse.andandorshort-circuit, which is what makesif x and x.fieldsafe.- Falsy values:
False,None,0,0.0,"",[],{},(),set(). any()andall()collapse a sequence of conditions into one answer, and stop early.- Compare floats with
math.isclose, never with==.