Python: fix TypeError: NotImplemented should not be used in a boolean context

NotImplemented snek coming for ya.

Take this class, which implements __eq__() and derives __ne__() from it:

class Lemming:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        if not isinstance(other, Lemming):
            return NotImplemented
        return self.name == other.name

    def __ne__(self, other):
        return not self.__eq__(other)

This technique was common on Python 2, before Python 3.0 started deriving __ne__() automatically from __eq__() (release note).

Now compare a Lemming with something that isn’t one:

>>> p = Lemming("Lenny")
>>> p != "Lenny"
False

Ah, woops, that’s wrong. A Lemming is not equal to the plain string "Lenny", so != should return True.

If we run the same code on Python 3.9–3.13 with warnings enabled, we can see why:

$ python -W default example.py
/.../example.py:12: DeprecationWarning: NotImplemented should not be used in a boolean context
  return not self.__eq__(other)

And on Python 3.14+, it’s no longer just a warning:

>>> p != "Lenny"
Traceback (most recent call last):
  ...
TypeError: NotImplemented should not be used in a boolean context

What’s going on?

other is a string, not a Lemming, so self.__eq__(other) hits the isinstance() check and returns the special singleton NotImplemented, rather than True or False. __ne__() then does not NotImplemented, feeding that singleton into a boolean context.

NotImplemented isn’t False, None, or empty, so like most objects, it’s truthy by default. That means not NotImplemented evaluates to False, and that False becomes the (wrong) result of our != comparison. This “worked” only by accident: NotImplemented is meant to be returned from a comparison method, to tell Python “try something else”, not to be evaluated as a bool itself.

The warning was added in Python 3.9, per the release note:

Using NotImplemented in a boolean context has been deprecated, as it is almost exclusively the result of incorrect rich comparator implementations. It will be made a TypeError in a future version of Python. (Contributed by Josh Rosenberg in bpo-35712.)

Five years later, Python 3.14 delivered on that promise and turned the warning into a TypeError, per the release note:

Using NotImplemented in a boolean context will now raise a TypeError. This has raised a DeprecationWarning since Python 3.9. (Contributed by Jelle Zijlstra in gh-118767.)

So rather than silently giving the wrong answer, our Lemming example now fails loudly, right at the point where the mistake happens.

The fix

The fix for the above Lemming class is to remove the __ne__() method entirely:

-    def __ne__(self, other):
-        return not self.__eq__(other)

Then we’ll see:

>>> p = Lemming("Lenny")
>>> p != "Lenny"
True

Since Python 3, object provides a default __ne__() that calls __eq__() and correctly inverts its result, whilst still propagating NotImplemented onwards so Python can try the other object’s reflected method, or fall back to identity comparison. There is rarely a need to write a __ne__() method by hand anymore, unless you have specific custom behaviour that isn’t the inverse of __eq__().

Another case

This bug isn’t confined to hand-written classes. For years, a “clever” one-liner for dropping None values from a list was:

>>> values = [1, None, 2, None, 3]
>>> list(filter(None.__ne__, values))
[1, 2, 3]

This looks like it filters out None, and on Python 3.8 and earlier, it does, but only because two “wrongs” made a right:

  1. None.__ne__(other) returns NotImplemented for any other value that isn’t None itself.
  2. filter() then checks the truthiness of that return value directly, and NotImplemented is truthy, so non-None items pass through. For None itself, None.__ne__(None) correctly returns False, so it’s filtered out.

As above, this technique triggers a warning from Python 3.9, and raises TypeError from Python 3.14.

The fix is to write the check explicitly in a generator expression or list comprehension, as appropriate:

>>> [x for x in values if x is not None]
[1, 2, 3]

This technique is also about 15 times faster than the filter() version, since it avoids the overhead of calling a method (None.__ne__()) for every item in the list.

Fin

May your code be modernized and swift,

—Adam


😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: