This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: int() and math.trunc don't accept objects that only define __index__
Type: enhancement Stage: resolved
Components: Documentation Versions: Python 3.8, Python 3.7, Python 3.6
process
Status: closed Resolution: duplicate
Dependencies: Superseder: type() constructor should bind __int__ to __index__ when __index__ is defined and __int__ is not
View: 20092
Assigned To: docs@python Nosy List: Eric Appelt, cheryl.sabella, docs@python, mark.dickinson, ncoghlan, remi.lapeyre, serhiy.storchaka, xtreak
Priority: normal Keywords: patch

Created on 2018-03-10 08:57 by ncoghlan, last changed 2022-04-11 14:58 by admin. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 13106 closed remi.lapeyre, 2019-05-06 14:02
Messages (10)
msg313515 - (view) Author: Alyssa Coghlan (ncoghlan) * (Python committer) Date: 2018-03-10 08:57
(Note: I haven't categorised this yet, as I'm not sure how it *should* be categorised)

Back when the __index__/nb_index slot was added, the focus was on allowing 3rd party integer types to be used in places where potentially lossy conversion with __int__/nb_int *wasn't* permitted.

However, this has led to an anomaly where the lossless conversion method *isn't* tried implicitly for the potentially lossy int() and math.trunc() calls, but is tried automatically in other contexts:

```
>>> import math
>>> class MyInt:
...     def __index__(self):
...         return 42
... 
>>> int(MyInt())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string, a bytes-like object or a number, not 'MyInt'
>>> math.trunc(MyInt())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: type MyInt doesn't define __trunc__ method
>>> hex(MyInt())
'0x2a'
>>> len("a" * MyInt())
42

```

Supporting int() requires also setting `__int__`:

```
>>> MyInt.__int__ = MyInt.__index__
>>> int(MyInt())
42
```

Supporting math.trunc() requires also setting `__trunc__`:

```
>>> MyInt.__trunc__ = MyInt.__index__
>>> math.trunc(MyInt())
42
```

(This anomaly was noticed by Eric Appelt while updating the int() docs to cover the fallback to trying __trunc__ when __int__ isn't defined: https://github.com/python/cpython/pull/6022#issuecomment-371695913)
msg313522 - (view) Author: Alyssa Coghlan (ncoghlan) * (Python committer) Date: 2018-03-10 11:34
Marking this as a documentation enhancement request for now, but I think we should also consider changing the type creation behaviour in 3.8 to implicitly add __int__ and __trunc__ definitions when __index__ is defined, but they aren't.

That way, no behaviour will change for classes that explicitly define __int__ or __trunc__, but classes that only define __index__ without defining the other methods will behave more intuitively.
msg332819 - (view) Author: Rémi Lapeyre (remi.lapeyre) * Date: 2018-12-31 14:52
>I think we should also consider changing the type creation behaviour in 3.8

@ncoghlan is this what's being done in PyTypeReady?
msg333546 - (view) Author: Alyssa Coghlan (ncoghlan) * (Python committer) Date: 2019-01-13 07:47
@Rémi Aye, filling out derived slots is one of the things PyType_Ready does.

This would need a discussion on python-dev before going ahead and doing it though, as the closest equivalent we have to this right now is the "negative" derivation, where overriding __eq__ without overriding __hash__ implicitly marks the derived class as unhashable (look for "type->tp_hash = PyObject_HashNotImplemented;").
msg336044 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2019-02-20 06:56
See also issue33039.
msg336201 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2019-02-21 10:45
Is not this a duplicate of issue20092?
msg336234 - (view) Author: Rémi Lapeyre (remi.lapeyre) * Date: 2019-02-21 16:10
Yes it is. Thanks for finding that @Serhiy.

Since nobody objected to the change on the mailing list and people seem to agree in issue 20092:

    [R. David Murray]
    To summarize for anyone like me who didn't follow that issue: __index__ means the object can be losslessly converted to an int (is a true int), while __int__ may be an approximate conversion.  Thus it makes sense for an object to have an __int__ but not __index__, but vice-versa does not make sense.


I will post my patch tonight.
msg340453 - (view) Author: Cheryl Sabella (cheryl.sabella) * (Python committer) Date: 2019-04-17 22:43
Rémi,

Are you still working on the patch for this?  Thanks!
msg341505 - (view) Author: Rémi Lapeyre (remi.lapeyre) * Date: 2019-05-06 14:11
Hi Cheryl,

thanks for the ping.

I wasn't sure my patch was correct but reading typeobject.c:add_operators(), it is actually more straight-forward than I thought.


Serhiy Storchaka: This is indeed a duplicate of issue20092. I believe the solution proposed by Nick Coghlan is better than the one of Amitava Bhattacharyya, "adding a call to `nb_index` (if that slot exists) in `_PyLong_FromNbInt`" though.


One thing to note regarding the proposed patch: the following stops to work and raises a RecursionError since __index__ == __int__:

        class MyInt(int):
            def __index__(self):
                return int(self) + 1

I changed test_int_subclass_with_index() as `int(self) + 1` is the same thing as `self + 1` for int subclasses. I don't think this sort of code should appear in the wild but if you think it is important not to break compatibility here, I think I could check for number subclasses before overriding __index__.
msg341508 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2019-05-06 14:29
Then let to continue the discussion on the older issue which has larger discussion.
History
Date User Action Args
2022-04-11 14:58:58adminsetgithub: 77220
2019-05-06 14:29:29serhiy.storchakasetstatus: open -> closed
superseder: type() constructor should bind __int__ to __index__ when __index__ is defined and __int__ is not
messages: + msg341508

resolution: duplicate
stage: patch review -> resolved
2019-05-06 14:11:13remi.lapeyresetmessages: + msg341505
2019-05-06 14:02:00remi.lapeyresetkeywords: + patch
stage: needs patch -> patch review
pull_requests: + pull_request13020
2019-04-17 22:43:11cheryl.sabellasetnosy: + cheryl.sabella
messages: + msg340453
2019-02-21 16:10:40remi.lapeyresetmessages: + msg336234
2019-02-21 10:45:51serhiy.storchakasetmessages: + msg336201
2019-02-20 06:56:34serhiy.storchakasetnosy: + serhiy.storchaka
messages: + msg336044
2019-01-13 07:47:32ncoghlansetmessages: + msg333546
2018-12-31 14:52:24remi.lapeyresetnosy: + remi.lapeyre
messages: + msg332819
2018-09-22 17:00:28xtreaksetnosy: + xtreak
2018-03-10 11:34:44ncoghlansetassignee: docs@python
type: enhancement
components: + Documentation
versions: + Python 3.6, Python 3.7, Python 3.8
nosy: + docs@python

messages: + msg313522
stage: needs patch
2018-03-10 08:57:11ncoghlancreate