Skip to content

BUG: Fix sinc for custom objects #29580

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

fjosw
Copy link

@fjosw fjosw commented Aug 17, 2025

This PR introduces a simple fix for the sinc function which stopped working for minimal custom objects. The problem was introduced in #27784 / numpy 2.3 and is caused by an explicit check for the kind of the associated dtype in order to determine an appropriate eps. This PR makes the check more robust.

Minimal breaking example:

import numpy as np

class MyObject:

    def __init__(self, value):
        self.value = value

    def __rmul__(self, y):
        return self.value * y

number = MyObject(2)
print(np.sinc(number))

Fails with

Traceback (most recent call last):
  File ".../numpy_repro.py", line 12, in <module>
    print(np.sinc(number))
          ^^^^^^^^^^^^^^^
  File ".../python3.12/site-packages/numpy/lib/_function_base_impl.py", line 3852, in sinc
    eps = np.finfo(x.dtype).eps if x.dtype.kind == "f" else 1e-20
                                   ^^^^^^^
AttributeError: 'float' object has no attribute 'dtype'

@fjosw fjosw force-pushed the fix/sinc_for_dtype_object branch 2 times, most recently from cdedc3b to 728bd04 Compare August 17, 2025 16:53
Numpy 2.3 broke the sinc function for custom objects because of an
explicit dtype check for determining the appropriate eps.
This PR fixes this issue by more introducing a more cautious check.
@fjosw fjosw force-pushed the fix/sinc_for_dtype_object branch from 728bd04 to d9913da Compare August 17, 2025 16:54
if hasattr(x, "dtype") and getattr(x.dtype, "kind", None) == "f":
eps = np.finfo(x.dtype).eps
else:
eps = 1e-20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose if it is worth fixing it is worth adding a regression test inspired by your reproducer like below the fold.

--- a/numpy/lib/tests/test_function_base.py
+++ b/numpy/lib/tests/test_function_base.py
@@ -2363,6 +2363,19 @@ def test_masked(self):
 
 class TestSinc:
 
+    def test_sinc_obj(self):
+        class MyObject:
+
+            def __init__(self, value):
+                self.value = value
+
+            def __rmul__(self, y):
+                return self.value * y
+
+        number = MyObject(2)
+        assert_allclose(np.sinc(number), np.sinc(2))

But maybe best to wait for feedback from a more active dev before putting more time into it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should definitely add a test. I do still wonder if we can find a slightly nicer work-around for backporting, but can't think of one.

@jorenham
Copy link
Member

jorenham commented Aug 18, 2025

This way of using sinc is not (and never has been) documented. Before 2.3, these __rmul__ objects were accepted by accident; not by design. It's a clever hack, but it's not a feature.

If we were to actively restore support for this hack, and add regression tests for it, then we're effectively adding official support for it. That means that would also have to document it, and modify the typing stubs to reflect this. But because I haven't seen a convincing use-case, and am not able to think of one either, I don't think that we should. And even if we wouldn't document it, and just keep it something like an "informal feature", then we risk setting a precedent for hacks like these.

So long story short; I don't think we should let sinc such __rmul__ objects. It already accepts array-likes, which are already flexible enough in my view.

@fjosw
Copy link
Author

fjosw commented Aug 18, 2025

Thanks a lot for your feedback, I agree that this is a bit of hack and not really worth being officially supported.

I still think that the current way of determining eps in the sinc function is not clean. Playing a bit more with sinc and objects I observed that

np.sinc(np.array(1.0, dtype=object))

fails with the unintuitive

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../python3.12/site-packages/numpy/lib/_function_base_impl.py", line 3852, in sinc
    eps = np.finfo(x.dtype).eps if x.dtype.kind == "f" else 1e-20
                                   ^^^^^^^
AttributeError: 'float' object has no attribute 'dtype'

while I would expect this behaviour

>>> np.sinc(np.array([1.0], dtype=object))
AttributeError: 'float' object has no attribute 'sin'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../python3.12/site-packages/numpy/lib/_function_base_impl.py", line 3854, in sinc
    return sin(y) / y
           ^^^^^^
TypeError: loop of ufunc does not support argument 0 of type float which has no callable sin method

Not a big issue, though, so feel free to close the PR 👍

@seberg
Copy link
Member

seberg commented Aug 19, 2025

We should fix this, but by using np.multiply(x, pi, out=...) to ensure an array result. That said, that might break other objects that don't implement __array_ufunc__, so dunno if one should backport that.

EIDT: Although, dunno, there is some risk in backporting that, but this might be a bit annoying regression also.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Pending authors' response
Development

Successfully merging this pull request may close these issues.

4 participants