Skip to content

The return value of function math.isclose can be somewhat misleading #126165

Description

@Xiaokang2022

Documentation

The following example may be misleading to people who are not familiar with the function:

>>> math.isclose(math.sin(0), 0)
... True
>>> math.isclose(math.sin(math.pi), 0)
... False
>>> 

To a newbie, the above results seem ridiculous. Correspondingly, the result in numpy is as follows:

>>> numpy.isclose(math.sin(0), 0)
... np.True_
>>> numpy.isclose(math.sin(math.pi), 0)
... np.True_
>>> 

For reference, some values are given here:

>>> math.sin(0)
... 0.0
>>> math.sin(math.pi)
... 1.2246467991473532e-16
>>> 

I think, the reason for this is that the default value of parameter abs_tol for function math.isclose is too small. Here are the definitions of the two functions mentioned above:

math.isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)
numpy.isclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=False)

I think we might need to state this in the docstring of function math.isclose, or increase the value of the default parameter abs_tol appropriately.

By the way, the numpy version I'm using here is v2.1.

If there is something wrong with my previous content, please feel free to point it out, thank you!

CPython versions tested on:

3.13

Operating systems tested on:

Windows

Linked PRs

Activity

  1. added
    docsDocumentation in the Doc dir
    pendingThe issue will be closed if no feedback is provided
    and removed
    type-bugAn unexpected behavior, bug, or error
    on Oct 30, 2024
  2. skirpichev commented on Oct 30, 2024

    @skirpichev
    Member

    I think we should state this in the docstring of function math.isclose

    State what, eh? the math.isclose() docstring has a complete description of parameters and their meaning. Ditto the sphinx docs: https://docs.python.org/3.14/library/math.html#math.isclose.

    or increase the value of the default parameter abs_tol appropriately.

    I fail to see any arguments for this in your text. "Do as numpy does"?

    In the PEP 485 the default value was chosen because: "there is no value that is appropriate for the general case. It is impossible to know an appropriate value without knowing the likely values expected for a given use case. If all the values tested are on order of one, then a value of about 1e-9 might be appropriate, but that would be far too large if expected values are on order of 1e-9 or smaller."

    I'll relabel this as docs issue. Please describe how you would like to improve sphinx docs/docstrings.

    If you are going for change defaults - this will be a huge compatibility break. I think this should be discussed first on https://discuss.python.org/.

  3. Xiaokang2022 commented on Oct 30, 2024

    @Xiaokang2022
    ContributorAuthor

    Well, it was me who was negligent. Thank you for pointing out my problem.

  4. removed
    pendingThe issue will be closed if no feedback is provided
    on Oct 30, 2024
  5. blhsing commented on Oct 30, 2024

    @blhsing
    Contributor

    Personally I think a default value should be set with the most common or most intuitive use cases in mind, and the OP's example of math.isclose(math.sin(math.pi), 0) is False demonstrates well why the current default value of the abs_tol parameter doesn't meet that expectation.

    I find numpy.isclose's default value of 1e-08 for atol to be a reasonable choice because it is one digit more precise than a single-precision floating point number.

    But of course, as @skirpichev pointed out, changing the default value at this point would cause backwards compatibility issues, so it's probably best that we make math.isclose(math.sin(math.pi), 0, abs_tol=1e-08) a usage example in the documentation.

  6. skirpichev commented on Oct 30, 2024

    @skirpichev
    Member

    Well, it was me who was negligent.

    Not at all. Maybe it's a valid documentation issue and we can explicitly mention defaults in the docstring/sphinx docs (not just via function signature). I'm -0 on this, but we do such things in some functions, e.g.:

    >>> help(math.prod)
    Help on built-in function prod in module math:
    
    prod(iterable, /, *, start=1)
        Calculate the product of all the elements in the input iterable.
    
        The default start value for the product is 1.
    
        When the iterable is empty, return the start value.  This function is
        intended specifically for use with numeric values and may reject
        non-numeric types.
    

    So, if you have a concrete suggestion on how to improve docs - feel free to reopen.

  7. skirpichev commented on Oct 30, 2024

    @skirpichev
    Member

    I think a default value should be set with the most common or most intuitive use cases in mind, and the OP's example of math.isclose(math.sin(math.pi), 0) is False

    I don't think we can appeal to one single case. What's "most intuitive use cases"?! The PEP has much more generic argumentation.

    reasonable choice because it is one digit more precise than a single-precision floating point number.

    (IEEE single-precision floats have 6-9 decimal digits precision.)

    Why single-precision floats are special here at all? BTW probably now in most implementations Python's floats are double-precision IEEE floats.

    best that we make math.isclose(math.sin(math.pi), 0, abs_tol=1e-08) a usage example in the documentation.

    Most math's functions have no examples, but maybe it worth.

  8. blhsing commented on Oct 30, 2024

    @blhsing
    Contributor

    I don't think we can appeal to one single case. What's "most intuitive use cases"?! The PEP has much more generic argumentation.

    Why single-precision floats are special here at all? BTW probably now in most implementations Python's floats are double-precision IEEE floats.

    I'll quote the PEP as follows:
    https://peps.python.org/pep-0485/#absolute-tolerance-default

    It was suggested that comparing to zero is, in fact, a common use case (evidence suggest that the numpy functions are often used with zero). In this case, it would be desirable to have a “useful” default. Values around 1e-8 were suggested, being about half of floating point precision for values of around value 1.

    But then the PEP explains why in the end it chooses not to guess what the users most likely want, in the spirit of The Zen. I personally think it's better to have a more useful default but I can understand where the PEP's argument is coming from.

    (IEEE single-precision floats have 6-9 decimal digits precision.)

    Single-precision floats with 24 bits of precision:

    >>> math.log10(2**24)
    7.224719895935548
    >>> n = 1
    >>> while ctypes.c_float(1 + 10 ** -n).value != 1:
    ...             n += 1
    ...
    >>> n
    8
    

    Most math's functions have no examples, but maybe it worth.

    There are a few math functions that do have examples, and since the PEP also mentions that comparing to zero is a rather common use case, I think it is indeed worth adding an example of such a common use case to the doc.

  9. skirpichev commented on Oct 30, 2024

    @skirpichev
    Member

    Single-precision floats with 24 bits of precision:

    math.log10(2**24)
    7.224719895935548

    This is an example with IEEE doubles.

    Details
    >>> import ctypes
    >>> libm = ctypes.CDLL('libm.so.6')
    >>> libm.log10f.argtypes = [ctypes.c_float]
    >>> libm.log10f.restype = ctypes.c_float
    >>> libm.log10.argtypes = [ctypes.c_double]
    >>> libm.log10.restype = ctypes.c_double
    >>> libm.log10f(1<<24).hex()
    '0x1.ce61d00000000p+2'
    >>> libm.log10(1<<24).hex()
    '0x1.ce61cf8ef36fep+2'
    >>> from gmpy2 import *
    >>> set_context(ieee(32))
    >>> log10(1<<24)
    mpfr('7.22472',24)

    Edit:

    while ctypes.c_float(1 + 10 ** -n).value != 1:

    Try that with CPython's float's;)

    I think it is indeed worth adding an example of such a common use case to the doc.

    Feel free to open a pr.

  10. terryjreedy commented on Oct 30, 2024

    @terryjreedy
    Member

    The current abs_tol lead sentence assume a lot of user understanding of the meaning of 'useful for comparisons near zero' and the implication of the (not repeated) 0.0 default, given the function definition in the next paragraph: abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol). I suggest replacing

    abs_tol is the minimum absolute tolerance – useful for comparisons near zero. abs_tol must be at least zero.

    https://docs.python.org/3.14/library/math.html#math.isclose

    with the more explicit:

    abs_tol is the minimum absolute tolerance; it must be at least 0.0. When comparing to 0.0, the default abs_tol of 0.0 means that only 0.0 is close to 0.0. Pass a larger tolerance to allow for floating point inaccuracies.

    >>> from math import sin, pi, isclose
    >>> sin(pi)
    1.2246467991473532e-16  # May vary slightly on different systems.
    >>> isclose(sin(pi), 0.0)
    False
    >>> isclose(sin(pi), 0.0, abs_tol=1e-15)  # Define 'close to zero' as needed or desired.
    True
    
  11. 3 remaining items

  12. terryjreedy commented on Oct 31, 2024

    @terryjreedy
    Member

    @skirpichev To me a negative rel_tol makes no sense. The function returns abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol). Changing that would require a PEP.

  13. skirpichev commented on Oct 31, 2024

    @skirpichev
    Member

    To me a negative rel_tol makes no sense.

    I'm not proposing that nonsense. Rather we should adjust the error check (now it's common for rel_tol and abs_tol) per documentation. It should be rel_tol <= 0 -> raises rather than rel_tol < 0 -> raises, as it's now.

  14. terryjreedy commented on Oct 31, 2024

    @terryjreedy
    Member

    Sorry, I was mislead by 'docs also slightly inaccurate', implying to me 'doc should be changed'. I now understand you saying 'doc slightly conflicts with implementation, and doc is correct'. rel_tol = 0 with default abs_tol = 0 make isclose same as ==. Do you approve of proposed doc change?

  15. skirpichev commented on Oct 31, 2024

    @skirpichev
    Member

    rel_tol = 0 with default abs_tol = 0 make isclose same as ==.

    On another hand with abs_tol>0 - rel_tol=0 does make sense...

    Do you approve of proposed doc change?

    #126165 (comment)
    ?

    See PR.

  16. hauntsaninja commented on Nov 2, 2024

    @hauntsaninja
    Contributor

    Strong +1 on not making rel_tol=0 raise. rel_tol=0 abs_tol>0 is indeed useful.

  17. added a commit that references this issue on Nov 4, 2024
  18. added 2 commits that reference this issue on Nov 4, 2024
  19. added 2 commits that reference this issue on Nov 4, 2024
  20. skirpichev commented on Nov 4, 2024

    @skirpichev
    Member

    Unfortunately, merged commit lacks note about rel_tol. We should document it's nonnegative.

  21. terryjreedy commented on Nov 4, 2024

    @terryjreedy
    Member

    Checkin diff says "rel_tol must be greater than zero." was changed to "rel_tol must be nonnegative and less than 1.0." in both places.

  22. added a commit that references this issue on Dec 8, 2024
  23. added a commit that references this issue on Jan 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    docsDocumentation in the Doc dir

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions