Skip to content

ENH: np.vectorize hides signature during jupyter introspection. #29824

Open

Description

@jagerber48

Proposed new feature or change:

If I write a function in a python module or in my juptyer notebook, when I am writing to call that function in a jupyter notebook, I can press tab to see the function signature and docstring. This helps me quickly write code, especially if the function has many arguments. However if I have wrapped the function in np.vectorize I instead see documentation for the vectorize class. This spoils the benefits of the tab complete tooling available in jupyter and other IDE-like environments.

I would like if I could still see the docstring for my function even when it is wrapped in np.vectorize. I know there are tools like functools.wraps that can help with this sort of thing, but I also know that it can still be tricky to get the desired behavior. I wonder if this has been considered before for np.vectorize?

Activity

  1. seberg commented on Sep 29, 2025

    @seberg
    Member

    Hmmmm, interesting, np.vectorize does set __doc__ similar to np.mean (implementation detail, this gets also wrapped) and using functools.wraps doesn't seem sufficient to me?
    I must be missing something that I probably knew at some point. IPython does do this nicely for np.mean which also is a class (although defined in C?) but doens't show the class docs, but rather the __doc__ that was set (or maybe via __wrapped__).

    Curious, did you have a working invocation to fix it?

  2. jagerber48 commented on Sep 29, 2025

    @jagerber48
    Author

    @seberg thank you for your quick response and looking into it. No I don't currently have an invocation for this. I was curious to know the prior art within numpy before diving into figuring out what is going on. I see the same issue in Pycharm.

    Ok, I did a little more testing. So the docstring set for the function DOES appropriately show up. Originally my function had no docstring.

    However. what I really want to see is the function signature.

    Here is an example without np.vectorize where I can helpfully see the function signature

    Image

    Here is the same example with np.vectorize where I can't see the function signature. However, I can see the docstring.

    Image

    Here is np.mean where I can see the signature.

    Image

    So I would say I gave this issue the wrong title. Maybe I should say 'np.vectorize hides function signature during jupyter introspection`. Let me know if I should change it.

  3. changed the title [-]ENH: `np.vectorize` hides docstrings during jupyter introspection.[/-] [+]ENH: `np.vectorize` hides signature during jupyter introspection.[/+] on Sep 30, 2025
  4. seberg commented on Oct 1, 2025

    @seberg
    Member

    Ah, it's all there. Your doc string is just so short that it is hard to see...

    Anyway, I think this actually can be improved (also for _ArrayFunctionDispatcher, although maybe that would mean removing the docs completely).
    I think we can move the class docstring into the __init__. I think that'll render just as well, but then it shouldn't show up as Class Docstring and spam the output here.

  5. jagerber48 commented on Oct 1, 2025

    @jagerber48
    Author

    Ah, it's all there. Your doc string is just so short that it is hard to see...

    Anyway, I think this actually can be improved (also for _ArrayFunctionDispatcher, although maybe that would mean removing the docs completely). I think we can move the class docstring into the __init__. I think that'll render just as well, but then it shouldn't show up as Class Docstring and spam the output here.

    @seberg sorry, see my last post more carefully. It is not "all" there. The docstring is there, but the function signature is not there. The function signature is really what I'm working. A workaround is that I can put documentation about the function parameters inside the docstring which is obviously a good practice. However I'm wanting this for functions where I'm just doing fast turnaround work in a jupyter notebook and there's no need to bother with writing docstrings. Also there can be a desync between the real function signature and what is in the docstring. So what I really want is for the Signature: row in the tooltip to match the actual function signature like it does when I'm not using np.vectorize. See my examples above.

    But, separately, what you describe about getting the np.vectorize class docstring out of the tooltip would be a nice improvement.

  6. seberg commented on Oct 1, 2025

    @seberg
    Member

    @jagerber48 ah, yes... We should still copy over those other attributes (or use functools.update_wrapper although I have heard of opinions that it is a bit heavy weight). I got confused things about things due to missing the docs (them being short).

    I think both are valid improvements, I am not going to jump on to it, but if you or someone else wants to make a PR we should explore both.

  7. Abhi210 commented on Oct 2, 2025

    @Abhi210
    Contributor

    Hi, I have read the discussion and this looks like a great issue to start with. If @jagerber48 does not minds, I’d love to take this on as my first contribution to NumPy.

  8. jagerber48 commented on Oct 5, 2025

    @jagerber48
    Author

    Hi, I have read the discussion and this looks like a great issue to start with. If @jagerber48 does not minds, I’d love to take this on as my first contribution to NumPy.

    @Abhi210 That sounds great to me! I would be happy to help review the PR.

  9. Abhi210 commented on Oct 6, 2025

    @Abhi210
    Contributor

    Thank you, @jagerber48, for giving me this opportunity. This is my first contribution to any open-source project. So, here is what I did:

    Your issue (as I understood): When you decorated the Python function with np.vectorize and attempted to view the type hints for the function, you could not see the function signature.

    What I did: Since we wanted to see the signature of the decorated function, I used inspect.signature() on that function to retrieve its signature. This allows the Jupyter IDE to get the signature of the function. Here are the images describing the before vs after setup:

    Image

    The changes I did are in the file: numpy/lib/_function_base_impl.py on line 2492 (after self._doc=doc line):

    + try:
    +     self.__signature__ = inspect.signature(pyfunc)
    + except (ValueError, TypeError):
    +     pass
    

    Since we needed the inspect module, I had to import it at the start of this file.

    @jagerber48: Does this address what you were aiming for?
    @seberg: Is this the right approach, or would you prefer a different way to implement what @jagerber48 wanted? I used ChatGPT for some help regarding this issue. Further,

    I think we can move the class docstring into the init. I think that'll render just as well, but then it shouldn't appear as a Class Docstring and clutter the output here.

    I did not understand this statement. What do we intend to do here?

    Further, if you type np.vectorize, it will give the type hints for the np.vectorize function.

  10. jagerber48 commented on Oct 6, 2025

    @jagerber48
    Author

    @Abhi210 interesting. Thank you for taking a look at this. What you've done does technically satisfy my requirement about getting the correct signature to appear in the tooltip under Signature. However, I don't like that the "Call Signature" field has been added in your example.

    In my example for the non-wrapped version there were 4 fields in the tooltip. "Signature", "Docstring", "File, "Type". In my example the wrapped version had two additional fields, "String Form" and "Class Docstring".

    Expanding the scope of this issue a little bit, I might change the requirement to be something like: I want the tooltip for the wrapped callable to be the exact same as the tooltip for the non-wrapped callable. It is probably possible to get that right for this simple function, but it might be more complicated when you take into account that the wrapped thing might be some more complicated class, or a lambda or something. I guess after reading through the responses in this issue it sounds like there hasn't been too much thought into getting this kind of thing right yet, so I think there's a chance with just a little more work like what you've done, we can get something that works nicely. However two important things:

    1. @seberg, do you agree with the goal that the tooltip for the wrapped function should exactly match the tooltip for the non-wrapped function
    2. If you come up with a more thorough approach, it will be important to test it against more cases than just the simple function I've provided. The two examples that spring to mind is that it should work for more complicated user defined classes that are callable, and it should work for lambda functions. It should work for functions defined in the notebook like this one, but also for imported functions. These are all I can think of offhand, but there are probably other important test cases.
  11. seberg commented on Oct 6, 2025

    @seberg
    Member

    I would need to audit whether it isn't lying (I don't think it is, though. Vectorize doesn't add ufunc parameters, I believe). __annotations__ would be dubious, though, since the types definitely change.

    wrapped callable to be the exact same as the tooltip for the non-wrapped callable

    Well, type and repr isn't and shouldn't be. It isn't the same thing. If this was designed purely as a decorator use, I might view it differently, but vectorize isn't.

  12. Abhi210 commented on Oct 6, 2025

    @Abhi210
    Contributor

    Thank you, @jagerber48, for suggesting the test cases. I tested my changes over different types of function calls that you suggested: lambda functions, functions from another module, and callables inside the class. In all the cases, the tooltip shows the Signature of the function. However, the "Call Signature" is still present inside the tooltip. I am thinking of some ways to remove them. I am attaching an image showing the results of the change on a lambda function:

    Image

    Also, should we really want the tooltip for a decorated function to match a non-decorated function? For example, users may also need to refer to the documentation for np.vectorize to add more options to the decorator. So, in this sense, the additional fields in the tooltip might come in handy.

  13. jagerber48 commented on Oct 6, 2025

    @jagerber48
    Author

    @Abhi210 I probably need to look at the code to understand what it is exactly I want. Basically I understand now that the wrapped object is an instance of the np.vectorize class with the __call__ function set to call the wrapped function foo() in a vectorized way. So I think I want all the documentation about my foo() function to appear in place of any documentation for np.vectorize.__call__(). After this recent discussion, I think that means I want the signature of foo() to appear as the Call Signature for the wrapped version of foo().

    I think seberg's suggestion is to move the documentation that currently appears as the class docstring (appearing right under class vectorize: ... to be the documentation under vectorize.__init__(). This way when someone is invoking vectorize they will be able to see the documentation, but that documentation won't appear on the wrapped functions that are instances of the vectorize class. This will get rid of the long Class Docstring of the tooltip that (1) is irrelevant and (2) pollutes the tooltip, making it hard to find the actual information you care about your wrapped function.


    I'm looking at the vectorize source code a bit. Here are some things I'm thinking:

    1. I agree with @seberg that we could try moving the docstring nested under the vectorize class definition to be under vectorize.__init__.
    2. I see that self.__doc__ is set to pyfunc.__doc__. I wonder if we actually want to set self.__call__.__doc__ to pyfunc.__doc__.
    3. In your change so far you set self.__signature__ to inspect.signature(pyfunc). Again, maybe we want to actually be setting self.__call__.__signature to inspect.signature(pyfunc).

    I think this reflects the semantics we want and I think there's a good chance this will make the tooltip look the way we want.

  14. jagerber48 commented on Oct 6, 2025

    @jagerber48
    Author

    Ok, I see that you can't set self.__call__.__signature__, the proper thing to do is set self.__signature__ as you have already done. However, check out this example in a jupyter notebook.

    Image

    This tooltip is pretty good. I think it is more accurately reflecting what we want. However, I do consider it a wart that Call Signature is appearing with pretty much useless information. I also wonder if we're breaking the typical semantics by injecting the signature into Signature instead of Call Signature. Like I wonder if Signature typically reflects Init Signature whereas Call Signature is what is meant to reflect the signature of __call__().

  15. jagerber48 commented on Oct 7, 2025

    @jagerber48
    Author

    I've done some research on Call Signature vs Signature and I've done some testing in jupyter. It seems like ipython checks inspect.signature(foo.__call__) and inspect.signature(foo). If these are different it will display the former as Call Signature and the latter as Signature. @Abhi210 discovered we can spoof inspect.signature(foo) by assigning foo.__signature__. However, it is not possible to overwrite foo.__call__.__signature__ because foo.__call__ is a bound method.

    In some time I haven't been able to figure out a way to work around this issue.

    I would be happy to have "Signature" reflect what I am looking for and to have "Call Signature" just be superfluous and useful even though it's annoying.

  16. Abhi210 commented on Oct 7, 2025

    @Abhi210
    Contributor

    I tried using functools.update_wrapper to set attributes like __doc__, __name__, and __wrapped__ for the wrapped function. However, the "Call Signature" field still appears in the tooltip.

    As @jagerber48 noted, since foo.__call__ is a bound method, its __signature__ cannot be modified directly. Therefore, it appears that our current approach of spoofing inspect.signature(foo) by assigning it to foo.__signature_ is the most viable solution. The persistent "Call Signature" field appears to be an IPython-specific behavior rather than a NumPy issue.

    Following @seberg’s suggestion, I’ve also moved the vectorize class docstring inside the __init__ method to avoid it appearing as a “Class Docstring” in the tooltip.

    @seberg, @jagerber48 — could you please review and let me know if you think this is ready for a commit?

  17. Abhi210 commented on Oct 13, 2025

    @Abhi210
    Contributor

    Hi @jagerber48! Just checking in to see if you had a chance to review my last comment.

    I’ve implemented the suggested changes (moved the docstring into __init__, added __signature__ and __wrapped__ handling, and tested across multiple cases). If everything looks good to you, I’d be happy to open a PR for this.

    Thank you!

  18. jagerber48 commented on Oct 13, 2025

    @jagerber48
    Author

    @Abhi210 can you please make a PR so that we can discuss the code changes in detail? It would be helpful to see the code diff together with more screen captures from the jupyter tooltip.

  19. added a commit that references this issue on May 21, 2026
  20. added a commit that references this issue on Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions