Skip to content

stubtest false positive: @type_check_only subtypes of numpy.ufunc #20223

@jorenham

Description

@jorenham

NumPy's universal functions, commonly known as ufuncs, are instances of the numpy.ufunc class 1. See the docs for details: https://numpy.org/doc/stable/reference/ufuncs.html.

Over one-third of the non-type callables from the main numpy namespace are ufuncs
>>> import numpy as np
>>> np.__version__
'2.3.4'
>>> sum(1 for v in vars(np).values() if isinstance(v, np.ufunc))
106
>>> sum(1 for v in vars(np).values() if callable(v) and not isinstance(v, type | np.ufunc))
289

At runtime, it is impossible to subclass numpy.ufunc, and is therefore marked as @final in the stubs. The stubs are (since a month) validated using stubtest, and I've put a lot of effort into minimizing the allowlist. But as you can see, the majority of this allowlist is made up of these ufuncs.

The callable signaature of the numpy.ufunc type is too flexible to be able to meaningfully annotate in the stubs. To get around that, NumPy has stubbed fictional @type_check_only subtypes of numpy.ufunc 2, in numpy/_typing/_ufunc.pyi, to be precise. For example, for unary ufuncs such as numpy.exp, there's _UFunc_Nin1_Nout1, and for binary ufuncs like numpy.pow, there's _UFunc_Nin2_Nout1. These are purely meant for internal usage.
With this, the ufuncs are defined in numpy/__init__.pyi as e.g. add: _UFunc_Nin2_Nout1[<irrelevant>] 3.

I expected that stubtest would have understood that these @type_check_only classes are fictional, and should therefore considered as fully transparent w.r.t. its supertype, numpy.ufunc. But as the allowlist entries already showed, that isn't what's going on. Instead, stubtest reports errors 4 for each of these ufuncs. To illustrate, this is what stubtest reports (excluding 6 duplicate errors) if I remove the allowlist entry for equal (pun intended):

error: numpy.equal variable differs from runtime type numpy.ufunc
Stub: in file /home/joren/Workspace/numpy/numpy/__init__.pyi:5802
numpy._typing._ufunc._UFunc_Nin2_Nout1[Literal['equal'], Literal[23], None]
Runtime:
def (*args, **kwargs)

Note that this does not only affect NumPy, but also SciPy, which has even more ufuncs than NumPy has in scipy.special, as can be seen in the allowlist at scipy-stubs/.mypyignore. It's pretty much the same @type_check_only-ufunc-subtype problem as from NumPy, but on a larger scale. They're defined in scipy-stubs/special/_ufuncs.pyi, but I should warn you that the 3k LOC overload-spaghetti there isn't for the faint of heart 😅.

You might wonder why do I care; seeing as the allowlist entries are doing their job, and users wouldn't notice the difference. But the issue with allowlist entries is that they leave a blind spot. Meaning that I wouldn't know if a ufunc was changed or, gods forbid, disappeared. And at the moment, these form the biggest static-typing blind-spot for both numpy and scipy[-stubs].

BTTW, if you think that someone largely unfamiliar with the mypy and stubtest codebase should be able to able to implement/fix this, then I wouldn't mind giving it a shot. I also wouldn't mind if someone else wants to work on this, either :)

Note that this is related to #15146. Maybe it'd help if this could be made into a sub-issue (which is valid because this issue is @type_check_only 😜)?

Footnotes

  1. numpy.ufunc instances cannot be constructed from Python, and must be defined using the NumPy C API.

  2. Note that this requires us to ignore the mypy error reporting that @final classes cannot be subclassed. Ideally, this wouldn't happen for @type_check_only subtypes, but I can live with the # type: ignore here, and it's out-of-scope for this issue.

  3. I admit that it would be better to have marked them as Final and with = ..., but that's irrelevant to this issue.

  4. Stubtest reports multiple duplicate errors per ufunc actually; one for each time it's re-exported: [stubtest] Type re-export creates duplicated error with invalid line number #15023

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions