Comparison Operators in Python: Complete Equality, Relational, and Logical Comparison Implementation Guide

Comparison operators in python

Comparison operators in python

I used to think comparison operators were the simplest part of Python — just ==, !=, <, >, and friends, nothing to think too hard about. Then I ran into is versus == confusion, chained comparisons that behaved differently than I expected, and comparisons between custom objects that needed special methods to even work. This guide covers everything I’ve learned, from the fundamentals to the internal mechanics.

The Core Comparison Operators

Python provides six standard comparison operators:

OperatorMeaning
==Equal to
!=Not equal to
>Greater than
<Less than
>=Greater than or equal to
<=Less than or equal to
a = 10
b = 20

print(a == b)   # False
print(a != b)   # True
print(a > b)    # False
print(a < b)    # True
print(a >= 10)  # True
print(b <= 20)  # True

Output:

False
True
False
True
True
True

Every comparison operator returns a Boolean value — True or False — which is why they’re the backbone of every if statement, while loop condition, and filtering operation I write.

Comparing Different Data Types

Comparison operators work across many data types, not just numbers:

print("apple" < "banana")     # string comparison, lexicographic
print([1, 2] == [1, 2])       # list equality, element-wise
print((1, 2) == (1, 2))       # tuple equality
print({1, 2} == {2, 1})       # set equality, order doesn't matter

Output:

True
True
True
True

String comparisons are lexicographic, meaning Python compares character by character based on Unicode code points, similar to alphabetical ordering but technically based on character codes.

print("Apple" < "apple")

Output:

True

This surprises people the first time they see it — uppercase letters have lower Unicode code points than lowercase letters, so "Apple" sorts before "apple".

== vs. is: The Distinction That Trips Everyone Up

This was the single biggest “aha” moment I had with comparisons. == checks for value equality — whether two objects represent the same value. is checks for identity — whether two variables point to the exact same object in memory.

a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)   # True, same values
print(a is b)   # False, different objects
print(a is c)   # True, same object

Output:

True
False
True

For small integers and interned strings, Python sometimes reuses the same object internally as a memory optimization, which can make is behave unexpectedly:

x = 256
y = 256
print(x is y)   # True, due to small integer caching

x = 257
y = 257
print(x is y)   # Often False, outside the cached range

Output:

True
False

I always tell people never to rely on this caching behavior intentionally — it’s a CPython implementation detail, not a language guarantee. For value comparisons, always use ==. Reserve is specifically for identity checks, most commonly is None.

value = None
if value is None:
    print("value is None")

Output:

value is None

Chained Comparisons

Python allows chaining comparison operators in a way that reads naturally and matches mathematical notation:

x = 5
print(1 < x < 10)

Output:

True

This is internally equivalent to 1 < x and x < 10, but Python evaluates x only once, which is both more efficient and avoids potential side effects from evaluating an expression twice.

a, b, c = 1, 2, 3
print(a < b < c)
print(a < b > c)

Output:

True
False

How Comparisons Work Internally

When I write a == b, Python calls a.__eq__(b) behind the scenes. If that returns NotImplemented, Python then tries b.__eq__(a). This is why comparison behavior can be customized for user-defined classes:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

p1 = Point(1, 2)
p2 = Point(1, 2)

print(p1 == p2)

Output:

True

Without defining __eq__, Python’s default behavior for custom objects falls back to identity comparison, meaning p1 == p2 would be False even with identical attribute values, since it would essentially behave like is.

Similarly, <, >, <=, and >= map to __lt__, __gt__, __le__, and __ge__ respectively. I can implement these individually, or use the functools.total_ordering decorator to fill in the rest automatically once I’ve defined __eq__ and one ordering method.

from functools import total_ordering

@total_ordering
class Money:
    def __init__(self, amount):
        self.amount = amount

    def __eq__(self, other):
        return self.amount == other.amount

    def __lt__(self, other):
        return self.amount < other.amount

a = Money(10)
b = Money(20)

print(a < b)
print(a > b)
print(a <= b)

Output:

True
False
True

Comparing Objects of Different Types

Comparing genuinely incompatible types raises an error in modern Python:

print(5 < "5")

Output:

TypeError: '<' not supported between instances of 'int' and 'str'

Equality comparisons between different types, on the other hand, don’t raise errors — they simply return False unless the types define compatible comparison logic:

print(5 == "5")

Output:

False

Comparing Floating-Point Numbers Safely

This is a mistake I made constantly early on — comparing floats directly with == can fail due to how floating-point numbers are represented in binary.

print(0.1 + 0.2 == 0.3)

Output:

False

Instead, I use math.isclose() for float comparisons:

import math
print(math.isclose(0.1 + 0.2, 0.3))

Output:

True

Performance Considerations

Comparison operators on built-in types are implemented in C and are extremely fast. The main performance concern arises with custom __eq__ or __lt__ implementations that do expensive work — since sorting algorithms, for example, can call comparison methods O(n log n) times, an expensive comparison function can noticeably slow down sorting large collections.

data = [Money(x) for x in range(10000)]
data.sort()  # relies on __lt__ repeatedly

If __lt__ does something computationally heavy, this adds up quickly across many comparisons.

Common Mistakes

  1. Using is instead of == for value comparisons — Leads to bugs that only appear inconsistently, especially with strings and integers due to caching quirks.
  2. Comparing floats directly — Always use math.isclose() or a manual epsilon tolerance for floating-point equality.
  3. Forgetting that __eq__ without __hash__ disables hashing — If you define __eq__ on a custom class, Python sets __hash__ to None unless you also define it, which breaks usage in sets and dictionary keys.
  4. Assuming chained comparisons work like most other languages — In many other languages, 1 < 2 < 3 might not even compile or means something different; in Python it works intuitively but it’s still worth understanding the underlying mechanics.

Real-World Applications

Debugging Tips

When a comparison isn’t behaving the way I expect on custom objects, I check whether __eq__ (and related methods) are actually defined:

print(hasattr(MyClass, '__eq__'))
print(MyClass.__eq__)

I also print the types of both operands when a comparison seems off, since silent type mismatches are a common source of confusion.

Sorting With Custom Comparison Logic

Comparison operators power Python’s sorting functions directly. When I need custom sort order that doesn’t match the natural ordering of a type, I use the key parameter rather than trying to override comparison operators globally:

words = ["banana", "kiwi", "apple", "fig"]

# Sort by length instead of alphabetically
sorted_by_length = sorted(words, key=len)
print(sorted_by_length)

# Sort in reverse alphabetical order
sorted_reverse = sorted(words, reverse=True)
print(sorted_reverse)

Output:

['kiwi', 'fig', 'apple', 'banana']
['kiwi', 'fig', 'banana', 'apple']

Using key is almost always cleaner than manually implementing __lt__ just to support one specific sort order, since it keeps the natural comparison behavior of the object intact for other contexts.

Comparing Sequences Element by Element

Something I find genuinely elegant about Python is how sequence comparisons work lexicographically, comparing corresponding elements in order until a difference is found:

print([1, 2, 3] < [1, 2, 4])
print([1, 2] < [1, 2, 3])
print((1, "a") < (1, "b"))

Output:

True
True
True

This is exactly how Python compares strings character by character, and it extends naturally to lists and tuples, which is why sorting a list of tuples “just works” the way I’d intuitively expect, prioritizing the first element and only checking later elements to break ties.

FAQs

What’s the difference between == and is again? == compares values; is compares object identity (whether it’s literally the same object in memory).

Why does 0.1 + 0.2 == 0.3 return False? Because floating-point numbers are stored in binary and can’t represent many decimal fractions exactly, leading to tiny rounding differences.

Can I compare a list and a tuple with ==? No — even with identical elements, a list and a tuple are different types and == returns False between them.

What happens if I don’t define lt on a custom class but try to sort a list of them? Python raises a TypeError since it doesn’t know how to order them.

Summary

Comparison operators are foundational to nearly every piece of Python logic, and while the basics are simple, the deeper mechanics — the distinction between == and is, how custom objects hook into comparisons through dunder methods, and the pitfalls of comparing floats directly — are worth understanding thoroughly. Getting comfortable with these details has made my conditional logic and sorting code far more predictable and bug-free.

References

Exit mobile version