skip to content

Why does `3 < 'apple'` raise TypeError in Python 3 when Python 2 ordered them?

level: juniorimportance: must knowfreq 60%

answer

  1. Python 3 dropped a Python 2 fallback
  2. Both operands get asked, in turn
  3. The sentinel means not my job
  4. Reflected operation: a < b tries b > a
  5. Equality still answers False, ordering raises

basics

~10 s

Python 3 removed the default ordering between unrelated types. The int's less-than returns NotImplemented for a str, the reflected str greater-than returns NotImplemented too, and with no fallback left the interpreter raises TypeError.

solid answer

~40 s

Comparison is a two-step dispatch. Python calls `int.__lt__(3, 'apple')`, which returns the `NotImplemented` sentinel meaning "not my job"; it then tries the reflected operation `str.__gt__('apple', 3)`, which also returns `NotImplemented`. Both operands declined and Python 3 has no fallback ordering, so it raises `TypeError: '<' not supported between instances of 'int' and 'str'`. Python 2 did have such a fallback — a single three-way comparison hook plus an arbitrary total order across types, roughly numbers first and then by type name — so it answered `True` and happily "sorted" mixed lists into meaningless order. Python 3.0 deleted both, and 3.14 behaves the same. Note that `3 == 'apple'` still returns `False` rather than raising: equality falls back to identity, ordering has no such last resort.

code

python · 14 lines
python
print(int.__lt__(3, "apple"))    # NotImplemented
print(str.__gt__("apple", 3))    # NotImplemented

try:
    3 < "apple"
except TypeError as exc:
    print(exc)                   # '<' not supported between instances of 'int' and 'str'

print(3 == "apple")              # False - equality falls back to identity

try:
    sorted([1, "a", 2])
except TypeError as exc:
    print(exc)

go deeper

for a junior

Be ready to say that Python 3 refuses to order values of unrelated types and raises TypeError, that Python 2 answered with an arbitrary ordering instead, and that None is the offender you will hit most often when sorting real data.

for a middle

Explain the two-step dispatch out loud: the left operand's method, then the reflected method on the right operand, then TypeError. Say what the NotImplemented sentinel means and why equality falls back to identity while ordering cannot.

for a senior

Show where this surfaces in production — a loosely typed field that is usually a number, a None among integers — and argue for normalizing types at the ingest boundary rather than catching the TypeError, since the exception is telling you a record is malformed.

for a principal

Own the position that loud failure here is a feature: an implicit cross-type ordering makes corrupt data look sorted. Decide where type normalization belongs in a pipeline and how schema validation at the edge keeps ordering assumptions out of downstream code.

### What the interpreter actually does `3 < 'apple'` is a binary operation, and Python resolves every binary operation the same way: it asks each operand, in turn, whether it knows how to handle the other. For `<` the interpreter first calls the left operand's less-than method, `int.__lt__(3, 'apple')`. `int` has no idea how to order itself against text, so instead of guessing it returns the singleton `NotImplemented` — a sentinel that means "not my job", not "the answer is false". Python then tries the **reflected** operation: `a < b` is answered by `b > a`, so it calls `str.__gt__('apple', 3)`. `str` also returns `NotImplemented`. Both operands have now declined, no fallback ordering exists, and the interpreter raises `TypeError: '<' not supported between instances of 'int' and 'str'`. The six rich comparison methods pair up like this: `__lt__` reflects to `__gt__`, `__le__` reflects to `__ge__`, and `__eq__`/`__ne__` reflect to themselves. There is one wrinkle in the order of the two attempts: if the right operand's type is a proper subclass of the left operand's type and overrides the reflected method, Python gives the subclass the first try, so a subclass can always override its parent's ordering. ### Why Python 2 answered instead of raising Python 2 had two mechanisms Python 3 deleted. The first was a single three-way comparison hook — one method returning a negative number, zero or a positive number, the C-style `strcmp` shape — which the six rich comparison methods replaced. The second, and the one that produced the surprising answers, was a **default total ordering across unrelated types**: when neither operand knew what to do, Python 2 fell back to an arbitrary but internally consistent rule (numbers sorted before everything else, and remaining types were ordered by type name). So `3 < 'apple'` was `True`, and `sorted([1, 'a', None])` returned a list that was "sorted" in a way no reader could predict or depend on. It was consistent within one run, but meaningless — and it silently hid data bugs, which is exactly why Python 3.0 removed it. Since 3.0 the rule has not changed, and it is the same on 3.14. ### Ordering fails loudly, equality does not `3 == 'apple'` does **not** raise; it returns `False`. The dispatch is identical — both `__eq__` implementations return `NotImplemented` — but equality has a sensible last resort that ordering does not: two objects that cannot compare themselves are simply not the same object, so Python falls back to identity and answers `False`. There is no comparable last resort for `<`. This asymmetry is worth carrying into an interview because it explains a common surprise: a dictionary lookup or an `in` test over a mixed list works fine, while sorting the same list explodes. ### Where this bites in production Sorting is the usual trigger, because `sorted()`, `min()`, `max()` and `list.sort()` are built entirely out of `<`. In a log-ingest pipeline whose events arrive as loosely typed records, a field that is normally an integer occasionally arrives as a string — a hand-edited config, a producer that serialized an ID as text, an unparsed `null` that lands as `None`. Nothing fails on ingest. The failure appears only in the batch that happens to contain both shapes, and only once the sort reaches the pair that cannot be compared, so a job that spends 45 seconds warming up and reading input dies deep in the run with a message about types the caller never knowingly mixed. Note also that the exception can arrive partway through: `sorted()` builds a new list so your input is untouched, but `list.sort()` may leave the list in an unspecified order after raising. `None` is the single most common offender, because `None` supports no ordering at all — `None < 1` raises for exactly the same reason `'apple' < 1` does. ### What to do about it Normalize at the boundary. Decide what the field is, coerce or reject on the way in, and sort a homogeneous sequence — that is a data-quality fix, not a comparison fix. If mixed values are genuinely legitimate, define the ordering explicitly by mapping every element to one comparable shape before sorting, so the ordering is a decision you wrote down rather than an accident of the interpreter. Never "fix" it by wrapping the sort in a `try`/`except TypeError`: that hides the malformed record instead of surfacing it. The same dispatch governs your own classes: when an instance of your type is compared with something it does not understand, it should hand the decision back with `NotImplemented` so the other operand gets its turn and, if nobody can answer, Python raises the standard, well-worded `TypeError` on your behalf. And `functools.total_ordering` is only a convenience on top of this machinery — it fills in the missing ordering methods from one you wrote, but it cannot invent an ordering between types that have none.

  • Why does `3 == 'apple'` return False instead of raising the way `<` does?
    The dispatch is the same — both `__eq__` implementations return `NotImplemented` — but equality has a sensible last resort that ordering lacks. When neither operand can answer, Python compares identity: they are not the same object, so the result is `False` (and `!=` gives `True`). There is no equivalent default for "less than", so ordering has nowhere to fall back to and raises `TypeError`.
  • If `sorted([1, 2.0, 3])` mixes int and float, why does that not raise?
    Because the numeric types cooperate rather than decline. `float.__lt__` and `int.__lt__` accept each other and compare by mathematical value, so neither returns `NotImplemented` and no fallback is needed. The rule is not "same type required" — it is "at least one operand must know how to order itself against the other". `int` and `str` fail that test; `int` and `float` pass it.
  • If a class defines only `__lt__`, does `a > b` work between two instances of it?
    Yes. Python tries `a.__gt__(b)` first; the inherited default returns `NotImplemented`, so it falls back to the reflected operation `b.__lt__(a)`, which the class does define. So `<` and `>` both work from one method, but `<=` and `>=` still raise `TypeError` because nothing reflects to them. `functools.total_ordering` exists to fill in the rest.

Python asks both operands "can you order yourself against this?" like passing a form between two clerks. Python 2 kept a rubber stamp for whatever nobody would sign; Python 3 threw the stamp away and hands the form back as an error.

saying these in an interview costs you the question

  • Claiming Python compares by type name in Python 3
  • Saying NotImplemented means the comparison is False
  • Confusing NotImplemented with the NotImplementedError exception
  • Expecting `3 == 'apple'` to raise TypeError as well
  • Believing only identical types can ever be ordered
  • Silencing the TypeError with try/except around the sort

context