Comparing Python Objects
Explains Python's comparison operators and semantics, including rich comparisons, chained comparisons, and default equality/ordering behavior. Use when writing __eq__/__lt__/__gt__ methods, debugging comparison-related bugs, implementing sortable/comparable classes, or reasoning about how Python compares built-in types like numbers, sequences, and mixed types.
Python has 8 comparison operators, all with the same priority and left-to-right chaining:
Python< <= > >= == != is is not
Default behavior:
==/!=fall back to identity (is/is not) if no__eq__is defined.<,<=,>,>=raiseTypeErrorunless the type defines them (no default ordering).- Objects are compared using rich comparison methods, not a single
__cmp__.
To make a class fully comparable, implement __eq__ and one ordering method, then use functools.total_ordering:
Pythonfrom functools import total_ordering @total_ordering class Version: def __init__(self, major, minor): self.major, self.minor = major, minor def __eq__(self, other): if not isinstance(other, Version): return NotImplemented return (self.major, self.minor) == (other.major, other.minor) def __lt__(self, other): if not isinstance(other, Version): return NotImplemented return (self.major, self.minor) < (other.major, other.minor)
When implementing or debugging comparisons on a class:
- Decide if identity-based default (
is) is sufficient — if so, skip overrides. - Implement
__eq__returningNotImplementedfor incompatible types (notFalse). - Implement
__lt__(and optionally__le__,__gt__,__ge__) — or just__lt__/__eq__+@total_ordering. - Ensure consistency: if
a == b, thenhash(a) == hash(b)(define__hash__alongside__eq__, or set__hash__ = Nonefor unhashable/mutable types). - Test reflected operations:
a < bcallsb.__gt__(a)ifa.__lt__(b)returnsNotImplemented. - Check chained comparisons behave correctly (
a < b < cisa < b and b < c, each side evaluated once).
Rich comparison methods (in order Python tries them):
| Operator | Method |
|---|---|
< | __lt__ |
<= | __le__ |
> | __gt__ |
>= | __ge__ |
== | __eq__ |
!= | __ne__ (defaults to inverting __eq__) |
Reflection rule: For a OP b, if a's method returns NotImplemented, Python tries b's reflected method (< ↔ >, <= ↔ >=, ==/!= reflect to themselves). If both return NotImplemented, ==/!= fall back to identity; ordering operators raise TypeError.
Built-in type comparison behavior:
- Numbers (
int,float,complex,Decimal,Fraction): compared by mathematical value across types (1 == 1.0isTrue);complexonly supports==/!=, not ordering. - Sequences (same type, e.g. two
lists or twotuples): compared lexicographically — element-by-element, first mismatch decides, shorter-is-less if one is a prefix of the other. - Mixed sequence types (e.g.
listvstuple):==/!=work structurally only for same type; ordering between different sequence types raisesTypeError. - str, bytes, bytearray: lexicographic by underlying code points/byte values.
strandbytesare never equal or orderable to each other. - dict:
==/!=compare keys and values regardless of order; no ordering operators. - set/frozenset:
==is set equality;<,<=,>,>=mean subset/superset (partial order, not total — some sets are simply "not comparable" and both<and>returnFalse). - NaN:
float('nan') != float('nan')and all ordering comparisons with NaN areFalse.
Example 1: Chained comparison pitfall Input:
Python1 < 2 < 3
Output: True — equivalent to (1 < 2) and (2 < 3), 2 evaluated once, not (1 < 2) < 3 (True < 3).
Example 2: Mixed sequence ordering Input:
Python[1, 2] < (1, 2)
Output: TypeError: '<' not supported between instances of 'list' and 'tuple' — ordering requires same/compatible types even though == between them (structurally) would just be False, not an error.
Example 3: Set partial order Input:
Python{1, 2} < {2, 3}
Output: False (not a subset) — and {1, 2} > {2, 3} is also False. Sets are a partial order, so "not <" does not imply ">=".
Example 4: NotImplemented vs False
Input: A custom __eq__ compares against an unrelated type and returns False instead of NotImplemented.
Output: Breaks reflection — if the other object's __eq__ would have returned True, Python never gets the chance to try it, since False short-circuits the comparison as "handled."
- Always return
NotImplemented(notFalse/True) from comparison methods when the other operand's type is unsupported — this lets Python try the reflected method. - Use
functools.total_orderinginstead of hand-writing all six comparison methods. - If you define
__eq__, explicitly decide on__hash__: inherit/reuse it if the object is still hashable, or set__hash__ = Noneif mutable/unhashable. - Prefer
==overisfor value comparisons; reserveisfor singleton checks (None,True,False, enum members). - Remember sequence/number comparisons are element-wise/value-wise, not identity-wise — don't assume
==impliesis.
- Comparing
NaNand expecting equality/reflexivity —nan == nanisFalseby IEEE 754 design. - Assuming all types support ordering — many (complex numbers, dicts, mixed sequence types) only support
==/!=. - Returning
Falseinstead ofNotImplementedin custom__eq__/__lt__, silently breaking comparisons with subclasses or duck-typed objects. - Forgetting that
!=doesn't automatically invert a custom__eq__in old-style implementations — in modern Python it does by default, but overriding__eq__without checking__ne__behavior in edge cases (e.g., returningNotImplemented) can cause inconsistencies. - Treating set
</>as a total order — two sets can be simultaneously "not less than" and "not greater than" each other.