Skip to content
C Codeloom
Python

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.

·8 min read · By Yash Kesharwani
Intermediate 9 min read

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:

OperatorMeaning
==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?
  • is asks: 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 b returns a if a is falsy, otherwise b
  • a or b returns a if a is truthy, otherwise b

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 evaluated
  • True 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 tests
  • is, 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, is checks identity — use is None, not == None
  • Chained comparisons (a < b < c) read naturally and evaluate b once
  • and and or return one of their operands, enabling default-value patterns
  • Short-circuit evaluation lets you guard against unsafe operations
  • in/not in test membership; prefer sets or dicts for large lookups
  • Floats need tolerance-based comparison; True == 1 because bool is an int

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.