# Buggy code
def _get_ufunc_and_otypes(self, func, args):
    """Return (ufunc, otypes)."""
    # frompyfunc will fail if args is empty
    if not args:
        raise ValueError("args can not be empty")

    if self.otypes is not None:
        otypes = self.otypes
        nout = len(otypes)

        # Note logic here: We only *use* self._ufunc if func is self.pyfunc
        # even though we set self._ufunc regardless.
        if func is self.pyfunc and self._ufunc is not None:
            ufunc = self._ufunc
        else:
            ufunc = self._ufunc = frompyfunc(func, len(args), nout)
    else:
        # Get number of outputs and output types by calling the function on
        # the first entries of args.  We also cache the result to prevent
        # the subsequent call when the ufunc is evaluated.
        # Assumes that ufunc first evaluates the 0th elements in the input
        # arrays (the input values are not checked to ensure this)
        args = [asarray(arg) for arg in args]
        if builtins.any(arg.size == 0 for arg in args):
            raise ValueError(
                "cannot call `vectorize` on size 0 inputs unless `otypes` is set"
            )

        inputs = [arg.flat[0] for arg in args]
        outputs = func(*inputs)

        # Performance note: profiling indicates that -- for simple
        # functions at least -- this wrapping can almost double the
        # execution time.
        # Hence we make it optional.
        if self.cache:
            _cache = [outputs]

            def _func(*vargs):
                if _cache:
                    return _cache.pop()
                else:
                    return func(*vargs)
        else:
            _func = func

        if isinstance(outputs, tuple):
            nout = len(outputs)
        else:
            nout = 1
            outputs = (outputs,)

        otypes = "".join([asarray(outputs[_k]).dtype.char for _k in range(nout)])

        # Performance note: profiling indicates that creating the ufunc is
        # not a significant cost compared with wrapping so it seems not
        # worth trying to cache this.
        ufunc = frompyfunc(_func, len(args), nout)

    return ufunc, otypes


# Traceback (ValueError)
Traceback (most recent call last):
File "function.py", line 17, in <module>
g(np.arange(3.0))  # Crash!
File "/home/hylke/.local/lib/python3.8/site-packages/numpy/lib/function_base.py", line 2091, in __call__
return self._vectorize_call(func=func, args=vargs)
File "/home/hylke/.local/lib/python3.8/site-packages/numpy/lib/function_base.py", line 2167, in _vectorize_call
outputs = ufunc(*inputs)
ValueError: invalid number of arguments

# Fixed code
def _get_ufunc_and_otypes(self, func, args):
    """Return (ufunc, otypes)."""
    # frompyfunc will fail if args is empty
    if not args:
        raise ValueError("args can not be empty")

    if self.otypes is not None:
        otypes = self.otypes

        # self._ufunc is a dictionary whose keys are the number of
        # arguments (i.e. len(args)) and whose values are ufuncs created
        # by frompyfunc. len(args) can be different for different calls if
        # self.pyfunc has parameters with default values.  We only use the
        # cache when func is self.pyfunc, which occurs when the call uses
        # only positional arguments and no arguments are excluded.

        nin = len(args)
        nout = len(self.otypes)
        if func is not self.pyfunc or nin not in self._ufunc:
            ufunc = frompyfunc(func, nin, nout)
        else:
            ufunc = None  # We'll get it from self._ufunc
        if func is self.pyfunc:
            ufunc = self._ufunc.setdefault(nin, ufunc)
    else:
        # Get number of outputs and output types by calling the function on
        # the first entries of args.  We also cache the result to prevent
        # the subsequent call when the ufunc is evaluated.
        # Assumes that ufunc first evaluates the 0th elements in the input
        # arrays (the input values are not checked to ensure this)
        args = [asarray(arg) for arg in args]
        if builtins.any(arg.size == 0 for arg in args):
            raise ValueError(
                "cannot call `vectorize` on size 0 inputs unless `otypes` is set"
            )

        inputs = [arg.flat[0] for arg in args]
        outputs = func(*inputs)

        # Performance note: profiling indicates that -- for simple
        # functions at least -- this wrapping can almost double the
        # execution time.
        # Hence we make it optional.
        if self.cache:
            _cache = [outputs]

            def _func(*vargs):
                if _cache:
                    return _cache.pop()
                else:
                    return func(*vargs)
        else:
            _func = func

        if isinstance(outputs, tuple):
            nout = len(outputs)
        else:
            nout = 1
            outputs = (outputs,)

        otypes = "".join([asarray(outputs[_k]).dtype.char for _k in range(nout)])

        # Performance note: profiling indicates that creating the ufunc is
        # not a significant cost compared with wrapping so it seems not
        # worth trying to cache this.
        ufunc = frompyfunc(_func, len(args), nout)

    return ufunc, otypes