I've been researching dunder methods in preparation for my upcoming talk at PyTexas. Somehow this lead me to playing around with code like the following:

>>> x = range(50)
>>> x < 20

So, what's the value of x < 20?

I'll kill the suspense for you, it is False.

I know, it didn't make sense to me either. I don't know what comparing integers to lists means semantically, but I would like to know why the answer is False. Python must have some reason for this being False, and I figured it would be interesting to look into.

I thought the answer could potentially be relevant for my upcoming talk. After all, the implementation of the < operator is handled by the less-than dunder method for the list type.

Assumptions

  1. My first thought was the < operator was comparing with len() for the list, which would make x < 51 True. Result: wrong.

  2. OK, then I figured it was using the list's id. This would sort of make sense. Result: wrong again.

>>> id(x)
170461900
>>> x < 170461901
False

To the source!

At this point the sheer mystery drove me to download the Python 2.7 source. I could have scoured the Internet for a quick solution, but I haven't spent much time digging in the Python 2.7 source so now seemed like a good time to start.

I started my search by looking up the implementation for '<' in the list object. Now the fun of tracing C function pointers and macros begins...

I'll save you the time and just provide a summary of what I found. The comparison operation ends in the default_3way_compare function. Finally, the following snippet of code decides that our list is always greater than (not less than) all integers:

/* different type: compare type names; numbers are smaller */
if (PyNumber_Check(v))
    vname = "";
else
    vname = v->ob_type->tp_name;
if (PyNumber_Check(w))
    wname = "";
else
    wname = w->ob_type->tp_name;
c = strcmp(vname, wname);
if (c < 0)
    return -1;
if (c > 0)
    return 1;

Surprised? Confused? I was both.

TL;DR

The end result is that comparison is based on the actual name of the types! So, in our case List is greater alphabetically than Integer!

Not exactly the rationale I was hoping for, but at least we have an answer. However, now that we know what is happening see if you can figure out the answer to a few more puzzles:

>>> [] < ()
>>> [] < {}

Spoiler alert

A List is less than a Tuple, remember 'L' comes before 'T' in the alphabet, and of course by the same logic a List is NOT less than a Dict.

Easter Egg

None is really small in comparison:

/* None is smaller than anything */
if (v == Py_None)
    return -1;
if (w == Py_None)
    return 1;

Python 3

This is a bit weird, but wait there's more! What does Python 3 do in this case?

>>> x = range(50)
>>> x < 20
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: range() < int()
>>> [] < ()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: list() < tuple()
>>> [] < {}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: list() < dict()

Nice! This little quirk has been fixed. Now when you upgrade your awesome application to Python 3 comparing completely different types will complain loudly as it should. Then, you will be free to forget everything you learned in this post. :)

Suggested Reading

If your interested I later found a great stackoverflow post confirming all of the above research. You can also look into the Python documentation to find the same answer.

It's also worth nothing this is an implementation choice by CPython. Your results with other implementations of Python like pypy could be different.