Comparison and Logical Operators in Python
A complete guide to Python's comparison and logical operators — equality vs identity, chained comparisons, short-circuit evaluation, and common pitfalls.
What you'll learn
- ✓The six comparison operators and how they handle types
- ✓The crucial difference between == and is
- ✓How and, or, and not really work — including their return values
- ✓How short-circuit evaluation lets you write safer code
- ✓Common operator pitfalls and how to avoid them
Prerequisites
- •Comfortable with if/elif/else — see Conditionals
- •Familiarity with Python data types — see Data Types
Comparison and logical operators are the building blocks of every condition your code evaluates. They look simple, and most of the time they are — but Python’s behaviour around equality, identity, and short-circuit evaluation has a few details that catch even experienced developers. This post covers them all.
The six comparison operators
Python has six comparison operators. Each returns True or False:
| Operator | Meaning |
|---|---|
== | equal to |
!= | not equal to |
< | less than |
<= | less than or equal to |
> | greater than |
>= | greater than or equal to |
They work on numbers as you would expect:
print(5 == 5) # True
print(5 != 3) # True
print(2 < 10) # True
print(7 >= 7) # True
They also work on strings, which compare lexicographically (character by character, using Unicode code points):
print("apple" < "banana") # True
print("Zebra" < "apple") # True — uppercase letters come before lowercase
If string ordering surprises you, that is usually the cause. For case-insensitive comparison, normalise first with .lower().
Lists and tuples compare element by element:
print([1, 2, 3] < [1, 2, 4]) # True
print((1, 2) == (1, 2)) # True
Mixing incompatible types raises TypeError for ordering, but == between incompatible types simply returns False:
print("3" == 3) # False — different types, no error
# print("3" < 3) # TypeError: '<' not supported between instances of 'str' and 'int'
Equality vs identity: == vs is
This distinction trips up nearly everyone at some point.
==asks: do these have the same value?isasks: are these the exact same object in memory?
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True — same contents
print(a is b) # False — different objects
print(a is c) # True — c is just another name for a
For everyday value comparisons, use ==. Reserve is for comparing against the singletons None, True, and False:
if value is None:
...
value == None works, but is None is the idiomatic and slightly faster form.
You may notice that is sometimes appears to “work” for small integers and short strings — Python caches them, so identical literals can share the same object. That is an implementation detail, not a guarantee. Never rely on it.
Chained comparisons
Python lets you chain comparison operators in a way that reads like mathematics:
x = 5
print(1 < x < 10) # True
print(0 < x <= 5) # True
print(0 < x < 5) # False — x is not strictly less than 5
a < b < c is exactly equivalent to a < b and b < c, but with b evaluated only once. Chaining works with any mix of comparison operators:
a, b, c = 1, 2, 3
print(a < b < c) # True
print(a < b > 0) # True (a < b and b > 0)
Use chains for genuine range checks. Don’t get clever — a < b == c is legal but rarely worth the puzzle.
Logical operators: and, or, not
Python’s three logical operators are spelled as English words. They are case-sensitive and lowercase only.
age = 25
member = True
print(age >= 18 and member) # True
print(age < 18 or member) # True
print(not member) # False
These look like boolean operators, but they have a twist worth understanding: and and or return one of their operands, not necessarily True or False.
print(0 and "hello") # 0
print(1 and "hello") # 'hello'
print("" or "default") # 'default'
print("name" or "default") # 'name'
The rule:
a and breturnsaifais falsy, otherwiseba or breturnsaifais truthy, otherwiseb
That is exactly the same as their boolean meaning, but it lets you use or as a quick way to provide a default:
def greet(name):
name = name or "stranger"
print(f"Hello, {name}!")
greet("Alice") # Hello, Alice!
greet("") # Hello, stranger!
not, by contrast, always returns an actual True or False.
Short-circuit evaluation
and and or evaluate left to right and stop as soon as the answer is known.
False and ...— the right side is never evaluatedTrue or ...— the right side is never evaluated
This is not just an optimisation; it is the canonical way to guard a potentially-failing operation:
data = None
if data is not None and len(data) > 0:
print(data[0])
If data is not None is False, the len(data) call never runs. Reverse the order and you would crash on None. This pattern shows up constantly — get comfortable with it.
Try it yourself. Predict the output of each line, then run them:
print(3 < 5 < 7)
print("" or 0 or "fallback")
print([] and "never")
print(None is None)
print(0 == False)The last one is surprising — explain to yourself why it is True.
Membership and identity operators
Beyond the six comparisons, Python has two pairs of operators that round out the set:
in,not in— membership testsis,is not— identity tests
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits) # True
print("grape" not in fruits) # True
value = None
print(value is None) # True
print(value is not None) # False
in works on any iterable: strings, lists, tuples, sets, dictionaries (which check keys). For large lookups, a set or dict gives constant-time membership where a list is linear:
allowed = {"admin", "editor", "viewer"}
role = "editor"
print(role in allowed) # fast even with thousands of entries
Common pitfalls
A handful of small surprises trip people up.
True and False are integers. bool is a subclass of int, so True == 1 and False == 0. This is occasionally useful (sum([True, False, True]) is 2) but also explains some odd-looking results.
Comparing floats with == is fragile. Floating-point rounding makes exact equality unreliable:
print(0.1 + 0.2 == 0.3) # False
For floats, compare with a tolerance using math.isclose:
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
Chained or for “either of these” does not do what you might think:
day = "Saturday"
if day == "Saturday" or "Sunday": # always True!
print("Weekend")
The expression on the right is (day == "Saturday") or "Sunday". The string "Sunday" is truthy, so the whole condition is always true. The fix is if day in ("Saturday", "Sunday"): — see Python Conditionals for more.
Operator precedence. Comparisons bind tighter than and/or, which bind tighter than not. When in doubt, add parentheses — they make intent obvious and never hurt:
if (age >= 18 and citizen) or override:
...
Try it yourself. Write a function is_valid_age(value) that returns True only if value is an integer between 0 and 120 inclusive. Use a chained comparison and isinstance(value, int). Test it on -1, 25, 200, "30", and None.
A worked example
A small input validator pulling several of these together:
def validate_login(username, password, attempts):
if not username or not password:
return "Username and password required."
if not (3 <= len(username) <= 20):
return "Username must be 3-20 characters."
if len(password) < 8:
return "Password too short."
if attempts is not None and attempts >= 5:
return "Too many attempts. Try again later."
return "OK"
print(validate_login("al", "secret123", 0)) # Username must be 3-20 characters.
print(validate_login("alice", "secret123", None)) # OK
print(validate_login("alice", "secret123", 5)) # Too many attempts. Try again later.
Each line uses something from this post: not for truthiness, a chained comparison for the range, is not None for the identity check, and and short-circuiting safely.
Recap
You now know:
- The six comparison operators work on numbers, strings, and many sequences
==checks value,ischecks identity — useis None, not== None- Chained comparisons (
a < b < c) read naturally and evaluatebonce andandorreturn one of their operands, enabling default-value patterns- Short-circuit evaluation lets you guard against unsafe operations
in/not intest membership; prefer sets or dicts for large lookups- Floats need tolerance-based comparison;
True == 1becauseboolis anint
Next steps
Now that you can write clean conditions, the next step is repetition. The following post covers for loops and the range function — Python’s primary tools for iterating over data.
→ Next: For Loops and the range Function in Python
Questions or feedback? Email codeloomdevv@gmail.com.