Allow modifying message of TypeError from NotImplemented

Python raises TypeError when NotImplemented is returned from __add__, __invert__ etc. and __radd__ etc. aren't available. However, this disallows the customization of error messages. For example, in Python 3.8, __float__ etc. were removed from complex to allow methods like __rfloat__. But this makes abs(z) undiscoverable for many users. The original error message is very helpful. I suggest to make a way for this usage. Maybe NotImplementedType can accept *args, and NotImplemented can be callable, equal to __init__:
class A: def __add__(self, other): return NotImplemented # Just like before def __neg__(self, other): return NotImplemented(f'bad operand type for unary -: {type(self).__name__!r}; use ~ to invert') # <-- def __invert__(self): return ~5 a = A() a + 2 TypeError: unsupported operand type(s) for +: 'A' and 'int' -a TypeError: bad operand type for unary -: 'A'; use ~ to invert ~a -6

On Sat, Jun 19, 2021 at 03:30:05AM -0000, wyz23x2@163.com wrote:
Python raises TypeError when NotImplemented is returned from __add__, __invert__ etc. and __radd__ etc. aren't available.
Roughly correct. It's more complex than that.
However, this disallows the customization of error messages.
Given a binary operator like `+`, there are two objects involved. Which one do you think should get to customize the error message if neither object handles the plus operator? We can't customize error messages for syntax errors, attribute errors, import errors, value errors, etc. I'm not sure that type errors are special enough that they need to be customized.
For example, in Python 3.8, __float__ etc. were removed from complex to allow methods like __rfloat__.
Are you sure about that? This is in 3.9: >>> complex.__float__ <slot wrapper '__float__' of 'complex' objects> >>> complex.__rfloat__ Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: type object 'complex' has no attribute '__rfloat__'
But this makes abs(z) undiscoverable for many users.
The set of people who know about complex numbers but not about using `abs(z)` to get the magnitude of a complex number is surely very small.
The original error message is very helpful.
We have to go all the way back to Python 2.5 to get a hint to use abs(z): >>> float(1+2j) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't convert complex to float; use abs(z) I don't think Python 3 has ever given such a hint: the oldest version I have installed, 3.2, doesn't. In any case, if we really think this is important, we could special case complex numbers. But I don't think it is important. Remember that it isn't mandatory for error messages to explain everything the caller needs to know about a data type. People are permitted to read the documentation and even search the internet.
I suggest to make a way for this usage. Maybe NotImplementedType can accept *args, and NotImplemented can be callable, equal to __init__:
What would the result of calling NotImplemented be? If it is the immutable NotImplemented singleton then supplying any arguments would be a no-op. They would just be ignored. If it returns something else, then it wouldn't be the NotImplemented singleton and that would become the operator's result. If we made NotImplemented mutable, that would lead to difficulty with multi-threaded code. One thread could modify the object, changing the error message, before another thread made use of that message. Fixing that would require locks, which would have a big performance impact. I would expect that it would slow down almost every operator. If we changed NotImplemented to no longer be a singleton, that would break code that checks for it with the `is` operator, which is one of the two main uses for `is` (the other is checking for None). Making this work would be a big change for a very small benefit. *If* we thought that being able to customize TypeErrors was important, it would, I think, be easier to add a new protocol to operators. After going through the whole left-and-right operand dance and calling the appropriate methods, if neither operand supported the operator, then the interpreter could look at each operand in turn for a new dunder, and then call that to get the customized error message. The API for that dunder would be complicated, but here is my quick design. There are two cases, for unary functions and operators and for binary operators. The unary operator case: function or operator which calls `__OP__` would then look for a new dunder `__OP_error__` containing the error message. So: complex.__float_error__ = 'use abs(z) instead' The binary operator case: the operator which calls `__OP__` would then look for a new *callable* dunder that returns an error message. For example, matrices don't define division, so we could do: class Matrix: def __true_division_error__(self, type_of_other): if type_of_other is Matrix: # Matrix / Matrix not permitted return "matrix division not defined; try multiplying by the inverse matrix" If the `__OP_error__` dunder returns None or NotImplemented, or doesn't exist, no additional message is appended to the TypeError. This is pretty complicated, for not a lot of benefit IMO. I think people should just learn to read the docs. -- Steve

On 19/06/2021 05:38, Steven D'Aprano wrote:
On Sat, Jun 19, 2021 at 03:30:05AM -0000, wyz23x2@163.com wrote:
Python raises TypeError when NotImplemented is returned from __add__, __invert__ etc. and __radd__ etc. aren't available. Roughly correct. It's more complex than that.
I just wanted to add that NotImplemented is *only* for binary (and binary comparison) operators. https://docs.python.org/3/library/constants.html?highlight=notimplemented#No... Ok, also when __length_hint__ can't help. Generally though, other contexts from which the special method might be called do not check for it. -- Jeff Allen

19.06.21 06:30, wyz23x2@163.com пише:
Python raises TypeError when NotImplemented is returned from __add__, __invert__ etc. and __radd__ etc. aren't available. However, this disallows the customization of error messages. For example, in Python 3.8, __float__ etc. were removed from complex to allow methods like __rfloat__. But this makes abs(z) undiscoverable for many users. The original error message is very helpful. I suggest to make a way for this usage. Maybe NotImplementedType can accept *args, and NotImplemented can be callable, equal to __init__:
class A: def __add__(self, other): return NotImplemented # Just like before def __neg__(self, other): return NotImplemented(f'bad operand type for unary -: {type(self).__name__!r}; use ~ to invert') # <-- def __invert__(self): return ~5 a = A() a + 2 TypeError: unsupported operand type(s) for +: 'A' and 'int' -a TypeError: bad operand type for unary -: 'A'; use ~ to invert ~a -6
NotImplemented is a singleton. And all code which tests for NotImplemented does it by checking identity. Returning any other object instead of the NotImplemented would not work.
participants (4)
-
Jeff Allen
-
Serhiy Storchaka
-
Steven D'Aprano
-
wyz23x2@163.com