[Python-Dev] The role of NotImplemented: What is it for and when should it be used?

Ethan Furman ethan at stoneleaf.us
Mon Nov 3 11:30:53 CET 2014


Just to be clear, this is about NotImplemented, not NotImplementedError.

tl;dr  When a binary operation fails, should an exception be raised or NotImplemented returned?


When a binary operation in Python is attempted, there are two possibilities:

   - it can work
   - it can't work

The main reason [1] that it can't work is that the two operands are of different types, and the first type does not know 
how to deal with the second type.

The question then becomes: how does the first type tell Python that it cannot perform the requested operation?  The most 
obvious answer is to raise an exception, and TypeError is a good candidate.  The problem with the exception raising 
approach is that once an exception is raised, Python doesn't try anything else to make the operation work.

What's wrong with that?  Well, the second type might know how to perform the operation, and in fact that is why we have 
the reflected special methods, such as __radd__ and __rmod__ -- but if the first type raises an exception the __rxxx__ 
methods will not be tried.

Okay, how can the first type tell Python that it cannot do what is requested, but to go ahead and check with the second 
type to see if it does?  That is where NotImplemented comes in -- if a special method (and only a special method) 
returns NotImplemented then Python will check to see if there is anything else it can do to make the operation succeed; 
if all attempts return NotImplemented, then Python itself will raise an appropriate exception [2].

In an effort to see how often NotImplemented is currently being returned I crafted a test script [3] to test the types 
bytes, bytearray, str, dict, list, tuple, Enum, Counter, defaultdict, deque, and OrderedDict with the operations for 
__add__, __and__, __floordiv__, __iadd__, __iand__, __ifloordiv__, __ilshift__, __imod__, __imul__, __ior__, __ipow__, 
__irshift__, __isub__, __itruediv__, __ixor__, __lshift__, __mod__, __mul__, __or__, __pow__, __rshift__, __sub__, 
__truediv__, and __xor__.

Here are the results of the 275 tests:
--------------------------------------------------------------------------------
testing control...

ipow -- Exception <unsupported operand type(s) for ** or pow(): 'Control' and 'subtype'> raised
errors in Control -- misunderstanding or bug?

testing types against a foreign class

iadd(Counter()) -- Exception <'SomeOtherClass' object has no attribute 'items'> raised instead of TypeError
iand(Counter()) -- NotImplemented not returned, TypeError not raised
ior(Counter()) -- Exception <'SomeOtherClass' object has no attribute 'items'> raised instead of TypeError
isub(Counter()) -- Exception <'SomeOtherClass' object has no attribute 'items'> raised instead of TypeError


testing types against a subclass

mod(str()) -- NotImplemented not returned, TypeError not raised

iadd(Counter()) -- Exception <'subtype' object has no attribute 'items'> raised (should have worked)
iand(Counter()) -- NotImplemented not returned, TypeError not raised
ior(Counter()) -- Exception <'subtype' object has no attribute 'items'> raised (should have worked)
isub(Counter()) -- Exception <'subtype' object has no attribute 'items'> raised (should have worked)
--------------------------------------------------------------------------------

Two observations:

   - __ipow__ doesn't seem to behave properly in the 3.x line (that error doesn't show up when testing against 2.7)

   - Counter should be returning NotImplemented instead of raising an AttributeError, for three reasons [4]:
     - a TypeError is more appropriate
     - subclasses /cannot/ work with the current implementation
     - __iand__ is currently a silent failure if the Counter is empty, and the other operand should trigger a failure

Back to the main point...

So, if my understanding is correct:

   - NotImplemented is used to signal Python that the requested operation could not be performed
   - it should be used by the binary special methods to signal type mismatch failure, so any subclass gets a chance to work.

Is my understanding correct?  Is this already in the docs somewhere, and I just missed it?

--
~Ethan~

[1] at least, it's the main reason in my code
[2] usually a TypeError, stating either that the operation is not supported, or the types are unorderable
[3] test script at the end
[4] https://bugs.python.org/issue22766 [returning NotImplemented was rejected]

-- 8< ----------------------------------------------------------------------------
from collections import Counter, defaultdict, deque, OrderedDict
from fractions import Fraction
from decimal import Decimal
from enum import Enum
import operator
import sys

py_ver = sys.version_info[:2]

types = (
     bytes, bytearray, str, dict, list, tuple,
     Enum, Counter, defaultdict, deque, OrderedDict,
     )
numeric_types = int, float, Decimal, Fraction

operators = (
     '__add__', '__and__', '__floordiv__',
     '__iadd__', '__iand__', '__ifloordiv__', '__ilshift__',
     '__imod__', '__imul__', '__ior__', '__ipow__',
     '__irshift__', '__isub__', '__itruediv__', '__ixor__',
     '__lshift__', '__mod__', '__mul__',
     '__or__', '__pow__', '__rshift__', '__sub__', '__truediv__',
     '__xor__',
     )

if py_ver >= (3, 0):
     operators += ('__gt__',  '__ge__', '__le__','__lt__')

ordered_reflections = {
         '__le__': '__ge__',
         '__lt__': '__gt__',
         '__ge__': '__le__',
         '__gt__': '__lt__',
         }


# helpers

class SomeOtherClass:
     """"
     used to test behavior when a different type is passed in to the
     special methods
     """
     def __repr__(self):
         return 'SomeOtherClass'
some_other_class = SomeOtherClass()

class MainClassHandled(Exception):
     """
     called by base class if both operands are of type base class
     """

class SubClassCalled(Exception):
     """
     called by reflected operations for testing
     """

def create_control(test_op):
     def _any(self, other):
         if not type(other) is self.__class__:
             return NotImplemented
         raise MainClassHandled
     class Control:
         "returns NotImplemented when other object is not supported"
     _any.__name__ = op
     setattr(Control, test_op, _any)
     return Control()

def create_subtype(test_op, base_class=object):
     def _any(*a):
         global subclass_called
         subclass_called = True
         raise SubClassCalled
     class subtype(base_class):
         __add__ = __sub__ = __mul__ = __truediv__ = __floordiv__ = _any
         __mod__ = __divmod__ = __pow__ = __lshift__ = __rshift__ = _any
         __and__ = __xor__ = __or__ = _any
         __radd__ = __rsub__ = __rmul__ = __rtruediv__ = __rfloordiv__ = _any
         __rmod__ = __rdivmod__ = __rpow__ = __rlshift__ = __rrshift__ = _any
         __rand__ = __rxor__ = __ror__ = _any
         __le__ = __lt__ = __gt__ = __ge__ = _any
     if issubclass(subtype, (bytes, bytearray)):
         value = b'hello'
     elif issubclass(subtype, str):
         value = 'goodbye'
     elif issubclass(subtype, (list, tuple)):
         value = (1, 2, 3)
     elif issubclass(subtype, (int, float, Decimal, Fraction)):
         value = 42
     else:
         # ignore value
         return subtype()
     return subtype(value)


# test exceptions

# control against some other class
print('testing control...\n')
errors = False
for op in operators:
     control = create_control(op)
     op = getattr(operator, op)
     try:
         op(control, some_other_class)
     except TypeError:
         # the end result of no method existing, or each method called returning
         # NotImplemented because it does not know how to perform the requested
         # operation between the two types
         pass
     except Exception as exc:
         errors = True
         print('%s(%s()) -- Exception <%s> raised instead of TypeError' %
                 (op.__name__, test_type.__name__, exc))
     else:
         errors = True
         print('Control -- TypeError not raised for op %r' % op)
if errors:
     print('errors in Control -- misunderstanding or bug?\n')

# control against a subclass
errors = False
for op in operators:
     subclass_called = False
     control = create_control(op)
     subtype = create_subtype(op, control.__class__)
     op = getattr(operator, op)
     try:
         op(control, subtype)
     except SubClassCalled:
         # if the control class properly signals that it doesn't know how to
         # perform the operation, of if Python notices that a reflected
         # operation exists, we get here (which is good)
         pass
     except MainClassHandled:
         errors = True
         print('Control did not yield to subclass for op %r' % op)
     except Exception as exc:
         if subclass_called:
             # exception was subverted to something more appropriate (like
             # unorderable types)
             pass
         errors = True
         print('%s -- Exception <%s> raised' %
                 (op.__name__, exc))
     else:
         errors = True
         print('Control -- op %r appears to have succeeded (it should not have)' % op)
if errors:
     print('errors in Control -- misunderstanding or bug?\n')


# tests
print('testing types against a foreign class\n')
for test_type in types + numeric_types:
     errors = False
     for op in operators:
         op = getattr(operator, op)
         try:
             op(test_type(), some_other_class)
         except TypeError:
             pass
         except Exception as exc:
             errors = True
             print('%s(%s()) -- Exception <%s> raised instead of TypeError' %
                     (op.__name__, test_type.__name__, exc))
         else:
             print('%s(%s()) -- NotImplemented not returned, TypeError not raised' %
                    (op.__name__, test_type.__name__))
     if errors:
         print()

print()

# test subclasses
print('testing types against a subclass\n')
for test_type in types:
     errors = False
     for op in operators:
         subclass_called = False
         if not test_type.__dict__.get(op):
             continue
         subclass = create_subtype(op, test_type)
         op = getattr(operator, op)
         try:
             if test_type is str:
                 op('%s', subtype)
             else:
                 op(test_type(), subtype)
         except SubClassCalled:
             # expected, ignore
             pass
         except Exception as exc:
             if subclass_called:
                 # exception raised by subclass was changed
                 pass
             errors = True
             print('%s(%s()) -- Exception <%s> raised (should have worked)' %
                     (op.__name__, test_type.__name__, exc))
         else:
             errors = True
             print('%s(%s()) -- NotImplemented not returned, TypeError not raised' %
                     (op.__name__, test_type.__name__))
     if errors:
         print()
for test_type in numeric_types:
     errors = False
     for op in operators:
         subclass_called = False
         if not test_type.__dict__.get(op):
             continue
         subtype = create_subtype(op, test_type)
         op = getattr(operator, op)
         try:
             op(test_type(), subtype)
         except SubClassCalled:
             # expected, ignore
             pass
         except Exception as exc:
             if subclass_called:
                 # exception raised by subclass was changed
                 pass
             errors = True
             print('%s(%s()) -- Exception <%s> raised (should have worked)' %
                     (op.__name__, test_type.__name__, exc))
         else:
             errors = True
             print('%s(%s)) -- NotImplemented not returned' %
                     (op.__name__, test_type.__name__))
     if errors:
         print()
-- 8< ----------------------------------------------------------------------------


More information about the Python-Dev mailing list