How do comparison operators < and > work with a function as an operand?

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).


But, none of these methods work with function objects while the < and > operators do work. What goes on under the hood that makes this happen?

In default of any other sensible comparison, CPython in the 2.x series compares based on type name. (This is documented as an implementation detail, although there are some interesting exceptions which can only be found in the source.) In the 3.x series this will result in an exception.

The Python spec places some specific constraint on the behaviour in 2.x; comparison by type name is not the only permitted behaviour, and other implementations may do something else. It is not something to be relied on.