pythonpython-3.xstringboolean-expressionequivalence

Why does `if None.__eq__("a")` seem to evaluate to True (but not quite)?


If you execute the following statement in Python 3.7, it will (from my testing) print b:

if None.__eq__("a"):
    print("b")

However, None.__eq__("a") evaluates to NotImplemented.

Naturally, "a".__eq__("a") evaluates to True, and "b".__eq__("a") evaluates to False.

I initially discovered this when testing the return value of a function, but didn't return anything in the second case -- so, the function returned None.

What's going on here?


Solution

  • This is a great example of why the __dunder__ methods should not be used directly as they are quite often not appropriate replacements for their equivalent operators; you should use the == operator instead for equality comparisons, or in this special case, when checking for None, use is (skip to the bottom of the answer for more information).

    You've done

    None.__eq__('a')
    # NotImplemented
    

    Which returns NotImplemented since the types being compared are different. Consider another example where two objects with different types are being compared in this fashion, such as 1 and 'a'. Doing (1).__eq__('a') is also not correct, and will return NotImplemented. The right way to compare these two values for equality would be

    1 == 'a'
    # False
    

    What happens here is

    1. First, (1).__eq__('a') is tried, which returns NotImplemented. This indicates that the operation is not supported, so
    2. 'a'.__eq__(1) is called, which also returns the same NotImplemented. So,
    3. The objects are treated as if they are not the same, and False is returned.

    Here's a nice little MCVE using some custom classes to illustrate how this happens:

    class A:
        def __eq__(self, other):
            print('A.__eq__')
            return NotImplemented
    
    class B:
        def __eq__(self, other):
            print('B.__eq__')
            return NotImplemented
    
    class C:
        def __eq__(self, other):
            print('C.__eq__')
            return True
    
    a = A()
    b = B()
    c = C()
    
    print(a == b)
    # A.__eq__
    # B.__eq__
    # False
    
    print(a == c)
    # A.__eq__
    # C.__eq__
    # True
    
    print(c == a)
    # C.__eq__
    # True
    

    Of course, that doesn't explain why the operation returns true. This is because NotImplemented is actually a truthy value:

    bool(None.__eq__("a"))
    # True
    

    Same as,

    bool(NotImplemented)
    # True
    

    For more information on what values are considered truthy and falsy, see the docs section on Truth Value Testing, as well as this answer. It is worth noting here that NotImplemented is truthy, but it would have been a different story had the class defined a __bool__ or __len__ method that returned False or 0 respectively.


    If you want the functional equivalent of the == operator, use operator.eq:

    import operator
    operator.eq(1, 'a')
    # False
    

    However, as mentioned earlier, for this specific scenario, where you are checking for None, use is:

    var = 'a'
    var is None
    # False
    
    var2 = None
    var2 is None
    # True
    

    The functional equivalent of this is using operator.is_:

    operator.is_(var2, None)
    # True
    

    None is a special object, and only 1 version exists in memory at any point of time. IOW, it is the sole singleton of the NoneType class (but the same object may have any number of references). The PEP8 guidelines make this explicit:

    Comparisons to singletons like None should always be done with is or is not, never the equality operators.

    In summary, for singletons like None, a reference check with is is more appropriate, although both == and is will work just fine.