Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve duck type compatibility of int with float #100268

Open
JakubTesarek opened this issue Dec 15, 2022 · 10 comments
Open

Improve duck type compatibility of int with float #100268

JakubTesarek opened this issue Dec 15, 2022 · 10 comments
Labels
expert-typing type-feature A feature request or enhancement

Comments

@JakubTesarek
Copy link

JakubTesarek commented Dec 15, 2022

Feature or enhancement

Function arguments and variables annotated with float should not allow value of type int.

Pitch

PEP484 suggested that when an argument is annotated as having type float, an argument of type int is acceptable;. This allows this kind of typing to be valid:

x: float = 2

But int is not subtype of float and doesn't provide the same interface. Float provides methods that are not available in int:

  • is_integer
  • fromhex
  • hex

This violates LSP and is problematic especially with is_integer:

def method_requiring_whole_number_float(number: float):
    if number.is_integer():
        ...

This method clearly states that it requires float and as an author of such code, I would expect that is_integer would be available if my typing is correct.

There are workarounds (if int(number) == number:) but they render the is_integer useless as it can never be safely used.

Just adding the missing methods to int (or removing the extra methods from float) would not be valid solution as there are other problems stemming from the fact that int is not float. Eg.:

def split_whole_and_decimal(number: float) -> tuple[str, str]:
    return str(number).split('.')  # it's reasonable to expect that the output contains a dot `.` as  `float` does but `int` doesn't 

I'm proposing an errata to PEP484 that will remove when an argument is annotated as having type float, an argument of type int is acceptable;.

Linked PRs

@JakubTesarek JakubTesarek added the type-feature A feature request or enhancement label Dec 15, 2022
@AlexWaygood
Copy link
Member

AlexWaygood commented Dec 15, 2022

Issues like this need to be filed over at https://github.com/python/typing rather than on this issue tracker. Perhaps @JelleZijlstra or @gvanrossum can transfer it over.

Having said that, while I agree in principle with many of your points, I doubt we'll be changing this. It's been this way for many years now, and changing it now would be very breaking for a large number of users.

@JakubTesarek
Copy link
Author

JakubTesarek commented Dec 15, 2022

@AlexWaygood Thank you and sorry for the incorrect filing of this issue.

I agree that this has been part of Python since the beginning of support of typing. It would be a big BC break (affecting also all my projects).

I was thinking about sending a PR to mypy that would "fix" this using an optional switch. But then I realised that because of how the PEP484 is phrased, it would be incorrect to disallow assigning int to float. Maybe there's a way it could be rephrased so that it would allow for such option in Mypy? In current state, my PR would be almost certainly rejected.

@carljm carljm changed the title Arguments annotated with float should not allow value of type int. Arguments annotated with float should not allow value of type int Dec 15, 2022
@rhettinger
Copy link
Contributor

rhettinger commented Dec 15, 2022

While your premise it true, a float is not an int. I don't think this has a chance. The PEP 484 decision was intentional and pragmatic. It reflects what is done in a lot of real world code (functions that accept floats also accept ints). And subsequent to the PEP being approved, the decision proved to be useful in practice.

To change the decision would require broad discussion and buy in and perhaps another PEP. If you want to pursue that route, I recommend starting a discussion on the forums. Just expect that it will be an uphill battle. The original decisions wasn't made lightly.

@rhettinger rhettinger closed this as not planned Won't fix, can't repro, duplicate, stale Dec 16, 2022
@JelleZijlstra
Copy link
Member

JelleZijlstra commented Dec 16, 2022

I agree with what was said above, but a few more thoughts:

  • mypy could add an option to disable the int/float (and complex/float) promotion, but one complication would be that typeshed stubs are written under the assumption that float includes int. So it would have to continue to apply the promotion when checking calls to stdlib functions.
  • You raise a good point about methods that exist on float but not int. Let's see if we can resolve those differences:
    • int.is_integer could be added with the trivial behavior of always returning True. This seems harmless to me but others may have objections; feel free to open an issue requesting it be added. A similar precedent is complex.__complex__, which was added in Should we define complex.__complex__ and bytes.__bytes__? #68422.
    • float.fromhex is a classmethod. If we added int.fromhex and made it accept the same format as float.fromhex, it would have to return a float, which would be weird for an int classmethod; and if it accepted a different format, we wouldn't actually help int/float interoperability.
    • float.hex returns a hex representation that is specific to floats, and it would be weird to have this method on ints but return a float-specific representation.

@AlexWaygood
Copy link
Member

AlexWaygood commented Dec 16, 2022

  • int.is_integer could be added with the trivial behavior of always returning True. This seems harmless to me but others may have objections; feel free to open an issue requesting it be added. A similar precedent is complex.__complex__, which was added in Should we define complex.complex and bytes.bytes? #68422.

I'd be happy to see this added, and I agree it would lessen some of the pain here. Another precedent is the way that int objects have fairly pointless imag and real properties. I believe this is purely so they conform a little more closely with the numeric tower in the numbers module, which specifies that Integral numbers are a subtype of Complex numbers:

>>> x = 5
>>> x.real
5
>>> x.imag
0

@gvanrossum
Copy link
Member

gvanrossum commented Dec 16, 2022

Int is supposed to be duck type compatible with float. So yes, it should have all (instance) methods of float.

@AlexWaygood
Copy link
Member

AlexWaygood commented Dec 16, 2022

Yes, fromhex() is an alternative constructor and a classmethod, so is much less relevant to LSP concerns.

@hauntsaninja hauntsaninja changed the title Arguments annotated with float should not allow value of type int Improve duck type compatibility of int with float Dec 22, 2022
@hauntsaninja hauntsaninja reopened this Dec 22, 2022
@hauntsaninja
Copy link
Contributor

hauntsaninja commented Dec 22, 2022

I think there's consensus that is_integer should be added to int. This is safe, straightforward and has precedent with the complex attributes, like Alex mentions. I agree that we should leave hex alone.

This is brought up somewhat regularly on mypy's issue tracker. I think there's a little bit of an XY complaint happening here in that the actual behaviour that static typing users get confused by is isinstance(1, float) is False — and users don't actually really care about the is_integer method. Note that mypy's behaviour has recently started to reflect the runtime more accurately (see python/mypy#13781 ), which should help with these complaints.

OP also provides a split_whole_and_decimal example, but I don't find that compelling since float.__str__ will happily spit out 1e+20

@mdickinson
Copy link
Member

mdickinson commented Dec 23, 2022

For history, see also issue #70867 and the reverted PR #6121.

@mdickinson
Copy link
Member

mdickinson commented Dec 23, 2022

@hauntsaninja Do you have bandwidth to make a corresponding PR for adding is_integer to fractions.Fraction? (decimal.Decimal is more involved, and might be better left to another day).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
expert-typing type-feature A feature request or enhancement
Projects
None yet
Development

No branches or pull requests

7 participants