Comparing unrelated Python types¶
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:
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¶
-
My first thought was the
<operator was comparing withlen()for the list, which would makex < 51True. Result: wrong. -
OK, then I figured it was using the list's id. This would sort of make sense. Result: wrong again.
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:
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.