For future readers, I am posting this answer because @wim has placed a bounty on this question asserting @Marcin's answer is erroneous with the reasoning that function < int
will evaluate to False
, and not True
as would be expected if lexicographically ordered by type names.
The following answer should clarify some exceptions to the CPython implementation; however, it is only relevant for Python 2.x
, since this comparison now throws an exception in Python 3.x
+.
The Comparison Algorithm
Python's comparison algorithm is very intricate; when two types are incompatible for comparison using the type's built-in comparison function, it internally defaults to several different functions in an attempt to find a consistent ordering; the relevant one for this question is default_3way_compare(PyObject *v, PyObject *w)
.
The implementation for default_3way_compare
performs the comparison (using lexicographical ordering) on the type's object names instead of their actual values (e.g. if types a
and b
are not compatible in a < b
, it analogously performs type(a).__name__ < type(b).__name__
internally in the C code).
However, there are a few exceptions that do not abide by this general rule:
None
: Always considered to be smaller (i.e. less) than any other value (excluding other None
's of course, as they are are all the same instance).
Numeric types (e.g. int
, float
, etc): Any type that returns a non-zero value from PyNumber_Check
(also documented here) will have their type's name resolved to the empty string ""
instead of their actual type name (e.g. "int", "float", etc). This entails that numeric types are ordered before any other type (excluding NoneType
). This does not appear to apply to the complex
type.
For example, when comparing a numeric type with a function with the statement 3 < foo()
, the comparison internally resolves to a string comparison of "" < "function"
, which is True
, despite that the expected general-case resolution "int" < "function"
is actually False
because of lexicographical ordering. This additional behavior is what prompted the aforementioned bounty, as it defies the expected lexicographical ordering of type names.
See the following REPL output for some interesting behavior:
>>> sorted([3, None, foo, len, list, 3.5, 1.5])
[None, 1.5, 3, 3.5, <built-in function len>, <function foo at 0x7f07578782d0>, <type 'list'>]
More example (in Python 2.7.17)
from pprint import pprint
def foo(): return 3
class Bar(float): pass
bar = Bar(1.5)
pprint(map(
lambda x: (x, type(x).__name__),
sorted(
[3, None, foo, len, list, -0.5, 0.5, True, False, bar]
)
))
output:
[(None, 'NoneType'),
(-0.5, 'float'),
(False, 'bool'),
(0.5, 'float'),
(True, 'bool'),
(1.5, 'Bar'),
(3, 'int'),
(<built-in function len>, 'builtin_function_or_method'),
(<function foo at 0x10c692e50>, 'function'),
(<type 'list'>, 'type')]
Additional Insight
Python's comparison algorithm is implemented within Object/object.c
's source code and invokes do_cmp(PyObject *v, PyObject *w)
for two objects being compared. Each PyObject
instance has a reference to its built-in PyTypeObject
type through py_object->ob_type
. PyTypeObject
"instances" are able to specify a tp_compare
comparison function that evaluates ordering for two objects of the same given PyTypeObject
; for example, int
's comparison function is registered here and implemented here. However, this comparison system does not support defining additional behavior between various incompatible types.
Python bridges this gap by implementing its own comparison algorithm for incompatible object types, implemented at do_cmp(PyObject *v, PyObject *w)
. There are three different attempts to compare types instead of using the object's tp_compare
implementation: try_rich_to_3way_compare
, try_3way_compare
, and finally default_3way_compare
(the implementation where we see this interesting behavior in this question).