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

R. David Murray rdmurray at bitdance.com
Mon Nov 3 14:32:38 CET 2014


See issue 22766 for some background on this question.

On Mon, 03 Nov 2014 02:30:53 -0800, Ethan Furman <ethan at stoneleaf.us> wrote:
> 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< ----------------------------------------------------------------------------
> _______________________________________________
> Python-Dev mailing list
> Python-Dev at python.org
> https://mail.python.org/mailman/listinfo/python-dev
> Unsubscribe: https://mail.python.org/mailman/options/python-dev/rdmurray%40bitdance.com


More information about the Python-Dev mailing list