Skip to content

UFunc

A large portion of NumPy's public API consists of universal functions, often denoted as ufuncs, which are (callable) instances of np.ufunc.

Tip

Custom ufuncs can be created using np.frompyfunc, but also through a user-defined class that implements the required attributes and methods (i.e., duck typing).

But np.ufunc has a big issue; it accepts no type parameters. This makes it very difficult to properly annotate its callable signature and its literal attributes (e.g. .nin and .identity).

This is where optype.numpy.UFunc comes into play: It's a runtime-checkable generic typing protocol, that has been thoroughly type- and unit-tested to ensure compatibility with all of numpy's ufunc definitions. Its generic type signature looks roughly like:

type UFunc[
    # The type of the (bound) `__call__` method.
    Fn: CanCall = CanCall,
    # The types of the `nin` and `nout` (readonly) attributes.
    # Within numpy these match either `Literal[1]` or `Literal[2]`.
    Nin: int = int,
    Nout: int = int,
    # The type of the `signature` (readonly) attribute;
    # Must be `None` unless this is a generalized ufunc (gufunc), e.g.
    # `np.matmul`.
    Sig: str | None = str | None,
    # The type of the `identity` (readonly) attribute (used in `.reduce`).
    # Unless `Nin: Literal[2]`, `Nout: Literal[1]`, and `Sig: None`,
    # this should always be `None`.
    # Note that `complex` also includes `bool | int | float`.
    Id: complex | bytes | str | None = float | None,
] = ...

Note

On older NumPy versions the extra callable methods of np.ufunc (at, reduce, reduceat, accumulate, and outer), are incorrectly annotated (as None attributes, even though at runtime they're methods that raise a ValueError when called). Until optype drops support for these older NumPy versions, it won't be possible to properly type these in optype.numpy.UFunc; doing so would make it incompatible with NumPy's ufuncs.