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: fraction.Fraction does not implement __int__.
Type: enhancement Stage: resolved
Components: Library (Lib) Versions: Python 3.11
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: Nosy List: josh.r, lukasz.langa, mamrhein, mark.dickinson, rhettinger, serhiy.storchaka
Priority: normal Keywords: patch

Created on 2021-07-01 19:52 by mamrhein, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 27851 merged mark.dickinson, 2021-08-20 09:58
Messages (15)
msg396827 - (view) Author: Michael Amrhein (mamrhein) Date: 2021-07-01 19:52
While int, float, complex and Decimal implement __int__, Fraction does not. Thus, checking for typing.SupportsInt for fractions fails, although int(<fraction>) succeeds, because Fraction implements __trunc__.
This looks inconsistent.
Easiest fix seems to be: Fraction.__int__ = Fraction.__trunc__
msg396840 - (view) Author: Josh Rosenberg (josh.r) * (Python triager) Date: 2021-07-02 02:25
Seems like an equally reasonable solution would be to make class's with __trunc__ but not __int__ automatically generate a __int__ in terms of __trunc__ (similar to __str__ using __repr__ when the latter is defined but not the former). The inconsistency is in both methods existing, but having the equivalence implemented in int() rather than in the type (thereby making SupportsInt behave unexpectedly, even though it's 100% true that obj.__int__() would fail).
msg396847 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2021-07-02 07:25
FWIW, there's some history here: there's a good reason that fractions.Fraction didn't originally implement __int__.

Back in the Bad Old Days, many Python functions that expected an integer would accept anything whose type implemented __int__ instead, and call __int__ to get the required integer. For example:

Python 3.7.10 (default, Jun  1 2021, 23:43:35) 
[Clang 11.0.3 (clang-1103.0.32.62)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from decimal import Decimal
>>> chr(Decimal("45.67"))
'-'

Effectively, __int__ was being used to mean two different things: (1) this value can be used as an integer, and (2) this value can be truncated to an integer. The solution was to introduce two new dedicated magic methods __index__ and __trunc__ for these two different meanings. See PEP 357 and PEP 3141 for some of the details. So adding __int__ to fractions.Fraction would have made things like `chr(Fraction("5/2"))` possible, too.

The behaviour above is still present (with a deprecation warning) in Python 3.9, and `chr(Decimal("45.67"))` has only finally been made a TypeError in Python 3.10.

We may now finally be in a state where ill-advised uses of __int__ in internal functions have all been deprecated and removed, so that it's safe to re-add __int__ methods.

But still, this seems mostly like an issue with the typing library. What is typing.SupportsInt intended to indicate? That an object can be used _as_ an integer, or that an object can be _truncated_ to an integer?
msg396856 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2021-07-02 11:42
I'm actually struggling to think of situations where typing.SupportsInt would be useful in its current form: if I'm writing a function that wants to do a duck-typed acceptance of integer-like things (for example because I want my function to work with NumPy integers as well as plain old Python ints) then I want an __index__ check rather than an __int__ check. If I'm writing a function that allows general numeric inputs, then I'm not sure why I'd be calling 'int' on those inputs.

As another data point, complex supporting __int__ is a little bit of an oddity, since all that __int__ method does is raise a TypeError.

@Michael: are you in a position to share the use-case that motivated opening the issue? I'd be interested to see any concrete uses of typing.SupportsInt.

Maybe typing.SupportsIndex (or typing.UsableAsInt, or ... --- naming things is hard) is what we actually need?

On this particular issue: I'm not opposed to adding __int__ to fractions.Fraction purely for the sake of consistency, but it's not yet clear to me that it solves any real issue.
msg396858 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2021-07-02 11:44
> Maybe typing.SupportsIndex

Apologies: that already exists, of course. It was introduced in #36972.
msg396861 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2021-07-02 11:54
> As another data point, complex supporting __int__ is a little bit of an oddity, since all that __int__ method does is raise a TypeError.

This was fixed in 3.10: #41974
msg396864 - (view) Author: Michael Amrhein (mamrhein) Date: 2021-07-02 13:36
The background is an implementation of __pow__ for a fixed-point decimal number:

SupportsIntOrFloat = Union[SupportsInt, SupportsFloat]

def __pow__(self, other: SupportsIntOrFloat, mod: Any = None) -> Complex:
    if isinstance(other, SupportsInt):
        exp = int(other)
        if exp == other:
            ... handle integer exponent
    if isinstance(other, SupportsFloat):
        # fractional exponent => fallback to float
        return float(self) ** float(other)
    return NotImplemented

I came across SupportsInt and SupportsFloat, because they are used in typeshed as param types for int and float.
msg396868 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2021-07-02 16:47
Thanks, that's helpful. I guess what you _really_ want there is a duck-typed "tell me whether this value is integral and if so give me the corresponding Python int", but that's not currently easily available, so I suppose x == int(x) is the next-best thing. Possibly the "right" way from the point of view of PEP 3141 is to be testing x == math.trunc(x) instead and asking for typing.SupportsTrunc, but it seems to me that __trunc__ never did really take off the way it was intended to.

tl;dr: I agree it would make sense to add __int__ to fractions.Fraction for 3.11.
msg396876 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2021-07-02 20:33
In ideal world we would use __int__ for (1), and __trunc__ for (2). But for some historical reasons __index__ was introduced for (1) and __int__ is only used in the int constructor, although it falls back to __trunc__.

I am wondering whether one of __int__ or __trunc__ should be deprecated.
msg397022 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2021-07-05 20:57
> I am wondering whether one of __int__ or __trunc__ should be deprecated.

I would not miss __trunc__.
msg397033 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2021-07-06 08:29
On other hand, there are classes which define __int__ but not __trunc__: UUID and IP4Address. So it makes sense to keep separate __int__ and __trunc__.
msg399958 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2021-08-20 10:36
Fraction.__int__ = Fraction.__trunc__ may not work because __trunc__() can return any object with __index__, while __int__ should return an exact int (not even an int subclass).
msg400060 - (view) Author: Mark Dickinson (mark.dickinson) * (Python committer) Date: 2021-08-22 09:03
I think there's no reason not to keep __trunc__ and math.trunc - they're natural counterparts to floor and ceil, and there's probably at least some code out there already using math.trunc.

It's the involvement of __trunc__ in the int() builtin that I'd quite like to deprecate and eventually remove. I think at this point it complicates the object model for no particularly good reason. But this is getting off-topic for this issue; I'll open a new one.
msg404682 - (view) Author: Łukasz Langa (lukasz.langa) * (Python committer) Date: 2021-10-21 22:09
New changeset d1b24775b462f4f28aa4929fd031899170793388 by Mark Dickinson in branch 'main':
bpo-44547: Make Fractions objects instances of typing.SupportsInt (GH-27851)
https://github.com/python/cpython/commit/d1b24775b462f4f28aa4929fd031899170793388
msg404683 - (view) Author: Łukasz Langa (lukasz.langa) * (Python committer) Date: 2021-10-21 22:10
Thanks for the patch, Mark! ✨ 🍰 ✨
History
Date User Action Args
2022-04-11 14:59:47adminsetgithub: 88713
2021-10-21 22:10:31lukasz.langasetstatus: open -> closed
resolution: fixed
messages: + msg404683

stage: patch review -> resolved
2021-10-21 22:09:51lukasz.langasetnosy: + lukasz.langa
messages: + msg404682
2021-08-22 09:03:03mark.dickinsonsetmessages: + msg400060
2021-08-20 10:36:25serhiy.storchakasetmessages: + msg399958
2021-08-20 09:58:28mark.dickinsonsetkeywords: + patch
stage: patch review
pull_requests: + pull_request26311
2021-07-06 08:29:47serhiy.storchakasetmessages: + msg397033
2021-07-05 20:57:23rhettingersetnosy: + rhettinger
messages: + msg397022
2021-07-02 20:33:50serhiy.storchakasetnosy: + serhiy.storchaka
messages: + msg396876
2021-07-02 16:47:22mark.dickinsonsetmessages: + msg396868
2021-07-02 13:36:09mamrheinsetmessages: + msg396864
2021-07-02 11:54:40mark.dickinsonsetmessages: + msg396861
2021-07-02 11:44:49mark.dickinsonsetmessages: + msg396858
2021-07-02 11:42:01mark.dickinsonsetmessages: + msg396856
2021-07-02 07:25:46mark.dickinsonsetversions: - Python 3.6, Python 3.7, Python 3.8, Python 3.9, Python 3.10
nosy: + mark.dickinson

messages: + msg396847

type: behavior -> enhancement
2021-07-02 02:25:52josh.rsetnosy: + josh.r
messages: + msg396840
2021-07-01 19:52:28mamrheincreate