Skip to content

approx equal can be more restricted than == #13218

Description

@yuyichao

This is similar to #13047 but is broader than that. In fact, the current PR that fixes that issue will only cause similar issues between other types. I'm very surprised that such major behavioral change may be introduced as a bug fix in a patch release.....

While I think it's wrong to make such change in a patch release, I do agree with using more exact comparison for boolean types. However, it seems over restricted when comparison using normal == produces True.

In [2]: 1 == pytest.approx(True)
Out[2]: False

In [3]: 1.0 == pytest.approx(True)
Out[3]: False

In [4]: 1.0 == True
Out[4]: True

In [5]: 1 == True
Out[5]: True

This makes the behavior of approx a bit difficult to reason about. I doubt if anyone would be asking for a more restricted comparison by adding approx...

Activity

  1. added a commit that references this issue on Feb 12, 2025
  2. Zac-HD commented on Mar 9, 2025

    @Zac-HD
    Member

    That changed recently - I actually think that the change is good, because I think it's much worse when tests pass-when-the-shouldn't than fail-when-they-shouldn't: in the latter case you find out and can do something about it.

    This should definitely be documented though, perhaps by adding a sentence to the top of the pytest.approx() docstring: "Note that unlike builtin equality, this function considers booleans unequal to numeric zero or one."

  3. added
    type: docsdocumentation improvement, missing or needing clarification
    good first issueeasy issue that is friendly to new contributor
    on Mar 9, 2025
  4. yuyichao commented on Mar 9, 2025

    @yuyichao
    Author

    I know that is is changed recently and as I said, I agree using exact comparisons for Boolean is reasonable (I think its borderline unacceptable to do so in a patch release or even a minor release but that’s a different issue).

    However, type exact comparisons is way more strict than exact equality which I believe was what the original issue was asking about. This introduce a significant inconsistency between Boolean type and other types that’s way more significant their corresponding value range would suggest. It’s unclear where this should end. Should integer get the same treatment? After all, they also take discrete values and not continuous ones.

    The argument that raising more error is good also doesn’t make any sense. If raising more error / returning false is seen as a benefit, then why not make everything that’s not floating point fail when using approx. After all, someone calling approx on integer or boolean may be doing so unintentionally so the safe thing to do here should be simply erroring out for all cases there any non floating point values and just require the user to do something about it instead. Also note that the “do something” could involve replicating approx’s logic with nested data structures e.g. for comparing [True] and [1].

    And fwiw, the fix #13132 for the linked issue shows how wrong the exact type comparison is. Unless there’s a standard way to query if a type should be treated as bool, adding special cases for third party types simply doesn’t make sense. I’m sure there are other libraries that implement such types (numba seems to have it previously). Are those types going to be added as special cases as well? If not, I don’t see why numpy should be singled out if it was not a direct dependency.

  5. TTsangSC commented on Mar 17, 2025

    @TTsangSC

    Hi, sorry for barging in, but here's my two cents as a fellow user.

    • Another somewhat counterintuitive edge case is how
      >>> 1 == pytest.approx(True)
      False
      >>> True == pytest.approx(1)
      True
      Approximate equality being in a sense not commutative sounds a bit wrong to me.
    • On the topic of booleans, I've done this in a personal project:
      def is_bool(x) -> bool:
          """Test boolean-like-ness via `str()` round-tripping."""
          as_str = str(x)
          values = {'True': True, 'False': False}
          try:
              return bool(x == values[as_str])
          except Exception:
              return False
      which does catch both builtins.bool and numpy.bool_ in a kinda logically consistent and dependency-agnostic way. (Of course it falls apart if someone implemented their booleans like def __str__(self) -> str: return 'T' if self else 'F', but still.)
    • While I do agree on principle that it doesn't make sense for numpy booleans to receive special treatment as a non-direct dep, FWIW the ship has sailed and much of pytest.approx is already engineered specifically with numpy compatibility in mind (e.g. _pytest.python_api.ApproxNumpy). So maybe special-casing numpy.bool_ isn't as bad (nor as much of) a precedent as it sounds, but a more general implementation (that is also more robust than what I have above) would be much welcome.
  6. callummscott commented on Apr 2, 2025

    @callummscott
    Contributor

    I was looking at this for a first issue and went to just change the docs but after reading them it seems already evident, although somewhat tucked away, that the approach that has been taken here --as also expressed in #9354-- is to fall back on strict equality in the case of "nonnumeric types". With that being said, I really don't see how that can be reconciled with @TTsangSC's edge case of

    >>> True == pytest.approx(1)
    True
    

    or likewise

    >>> False == pytest.approx(0)
    True
    

    It seems to me that this is actually a bug and warrants new tests. Would you agree?

  7. Zac-HD commented on Apr 2, 2025

    @Zac-HD
    Member

    I agree that both of your examples should return False (by symmetry), and would love new tests to that effect.

  8. natmokval commented on Apr 2, 2025

    @natmokval
    Contributor

    Hi @callummscott,

    Would you like to work on this issue, or I can try to fix it? I have already opened a PR for this issue and corrected the docstring for approx. However, I now see that there is more to address than just clarifying approx behavior.

  9. callummscott commented on Apr 2, 2025

    @callummscott
    Contributor

    So I've found out that tests have in fact already been added already in the previously mentioned pull request under the test_expecting_bool function, except that they've all been # noqa : E712 tagged. I think raising a separate issue for that matter is probably a good idea, which can improve on the changes in that aforementioned pr by just fixing the __eq__ method for ApproxScalar to account for its asymmetry.

    As for the issue initially raised here, @natmokval I think it's still just a matter of changing the docs. I saw that the reviewer in your pr said they thought it should be mentioned somewhere lower down and I wanted to change some of the spelling too while I was looking so if it's alright I've made a pr myself as well.

  10. natmokval commented on Apr 2, 2025

    @natmokval
    Contributor

    As for the issue initially raised here, @natmokval I think it's still just a matter of changing the docs. I saw that the reviewer in your pr said they thought it should be mentioned somewhere lower down and I wanted to change some of the spelling too while I was looking so if it's alright I've made a pr myself as well.

    Thanks @callummscott. I updated my PR and moved the mention that approx considers booleans unequal to numeric zero or one further down in the docstrings.

  11. added a commit that references this issue on Apr 4, 2025
    bf418b3
  12. added a commit that references this issue on Apr 4, 2025
    bb4d149
  13. added a commit that references this issue on Apr 6, 2025
    6964cf1
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

    good first issueeasy issue that is friendly to new contributortype: docsdocumentation improvement, missing or needing clarification

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions