Repository: numba/numba

Issue:
Numba RandomState overload fails with empty size
## Reporting a bug


- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/main/CHANGE_LOG).
- [x] I have included a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.

```python
import numpy as np
import numba

@numba.njit
def uniform():
    return np.random.uniform(0, 1, size=())

uniform()  # numba.core.errors.TypingError: Failed in nopython mode pipeline (step: nopython frontend)
```


Discussion:

Patch:
diff --git a/docs/upcoming_changes/9636.bug_fix.rst b/docs/upcoming_changes/9636.bug_fix.rst
new file mode 100644
index 000000000..df43fab4d
--- /dev/null
+++ b/docs/upcoming_changes/9636.bug_fix.rst
@@ -0,0 +1,6 @@
+Fix calls to ``numpy.random`` distributions with ``size=()``
+------------------------------------------------------------
+
+Calling any of the ``numpy.random`` distributions with ``size=()`` is
+now supported; previously it used to raise a ``TypingError`` while
+being supported by NumPy.
\ No newline at end of file
diff --git a/numba/core/cgutils.py b/numba/core/cgutils.py
index 780bcce13..04bb3cb5c 100644
--- a/numba/core/cgutils.py
+++ b/numba/core/cgutils.py
@@ -1222,6 +1222,14 @@ def is_nonelike(ty):
     )
 
 
+def is_empty_tuple(ty):
+    """ returns if 'ty' is an empty tuple """
+    return (
+        isinstance(ty, types.Tuple) and
+        len(ty.types) == 0
+    )
+
+
 def create_constant_array(ty, val):
     """
     Create an LLVM-constant of a fixed-length array from Python values.
diff --git a/numba/cpython/randomimpl.py b/numba/cpython/randomimpl.py
index fa1cba6a2..b1ad353f3 100644
--- a/numba/cpython/randomimpl.py
+++ b/numba/cpython/randomimpl.py
@@ -10,7 +10,7 @@ import numpy as np
 
 from llvmlite import ir
 
-from numba.core.cgutils import is_nonelike
+from numba.core.cgutils import is_nonelike, is_empty_tuple
 from numba.core.extending import intrinsic, overload, register_jitable
 from numba.core.imputils import (Registry, impl_ret_untracked,
                                     impl_ret_new_ref)
@@ -258,6 +258,9 @@ def random_impl0():
 def random_impl1(size=None):
     if is_nonelike(size):
         return lambda size=None: np.random.random()
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda size=None: np.array(np.random.random())
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                            and isinstance(size.dtype,
                                                           types.Integer)):
@@ -313,6 +316,9 @@ def np_gauss_impl2(loc, scale):
 def standard_normal_impl1(size):
     if is_nonelike(size):
         return lambda size: np.random.standard_normal()
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda size: np.array(np.random.standard_normal())
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                            isinstance(size.dtype,
                                                       types.Integer)):
@@ -331,6 +337,11 @@ def np_gauss_impl3(loc, scale, size):
             scale, (types.Float, types.Integer)) and
        is_nonelike(size)):
         return lambda loc, scale, size: np.random.normal(loc, scale)
+    if (isinstance(loc, (types.Float, types.Integer)) and isinstance(
+            scale, (types.Float, types.Integer)) and
+       is_empty_tuple(size)):
+        # Handle size = ()
+        return lambda loc, scale, size: np.array(np.random.normal(loc, scale))
     if (isinstance(loc, (types.Float, types.Integer)) and isinstance(
             scale, (types.Float, types.Integer)) and
        (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
@@ -600,6 +611,10 @@ def np_randint_impl_3(low, high, size):
     if (isinstance(low, types.Integer) and isinstance(high, types.Integer) and
        is_nonelike(size)):
         return lambda low, high, size: np.random.randint(low, high)
+    if (isinstance(low, types.Integer) and isinstance(high, types.Integer) and
+       is_empty_tuple(size)):
+        # Handle size = ()
+        return lambda low, high, size: np.array(np.random.randint(low, high))
     if (isinstance(low, types.Integer) and isinstance(high, types.Integer) and
        (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                             and isinstance(size.dtype,
@@ -665,6 +680,12 @@ def np_uniform_impl3(low, high, size):
             high, (types.Float, types.Integer)) and
        is_nonelike(size)):
         return lambda low, high, size: np.random.uniform(low, high)
+    if (isinstance(low, (types.Float, types.Integer)) and isinstance(
+            high, (types.Float, types.Integer)) and
+       is_empty_tuple(size)):
+        # When calling np.random.uniform with size = (), the returned value isn't a
+        # float like when size = None. Instead, it's an array of shape ()
+        return lambda low, high, size: np.array(np.random.uniform(low, high))
     if (isinstance(low, (types.Float, types.Integer)) and isinstance(
             high, (types.Float, types.Integer)) and
        (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
@@ -738,6 +759,11 @@ def triangular_impl(left, mode, right, size=None):
         return lambda left, mode, right, size=None: np.random.triangular(left,
                                                                          mode,
                                                                          right)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda left, mode, right, size=None: np.array(
+            np.random.triangular(left, mode, right)
+        )
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                             isinstance(size.dtype,
                                                        types.Integer))):
@@ -837,6 +863,9 @@ def _gammavariate_impl(_random):
 def gamma_impl(shape, scale, size):
     if is_nonelike(size):
         return lambda shape, scale, size: np.random.gamma(shape, scale)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda shape, scale, size: np.array(np.random.gamma(shape, scale))
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                            isinstance(size.dtype,
                                                       types.Integer)):
@@ -853,6 +882,9 @@ def gamma_impl(shape, scale, size):
 def standard_gamma_impl(shape, size):
     if is_nonelike(size):
         return lambda shape, size: np.random.standard_gamma(shape)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda shape, size: np.array(np.random.standard_gamma(shape))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                             and isinstance(size.dtype,
                                                            types.Integer))):
@@ -900,6 +932,10 @@ def _betavariate_impl(gamma):
 def beta_impl(a, b, size):
     if is_nonelike(size):
         return lambda a, b, size: np.random.beta(a, b)
+    if is_empty_tuple(size):
+        # When calling np.random.beta with size = (), the returned value isn't a
+        # float like when size = None. Instead, it's an array of shape ()
+        return lambda a, b, size: np.array(np.random.beta(a, b))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                             and isinstance(size.dtype,
                                                            types.Integer))):
@@ -940,6 +976,9 @@ def exponential_impl(scale):
 def exponential_impl(scale, size):
     if is_nonelike(size):
         return lambda scale, size: np.random.exponential(scale)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda scale, size: np.array(np.random.exponential(scale))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                             isinstance(size.dtype,
                                                        types.Integer))):
@@ -964,6 +1003,9 @@ def exponential_impl():
 def standard_exponential_impl(size):
     if is_nonelike(size):
         return lambda size: np.random.standard_exponential()
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda size: np.array(np.random.standard_exponential())
     if (isinstance(size, types.Integer) or
        (isinstance(size, types.UniTuple) and isinstance(size.dtype,
                                                         types.Integer))
@@ -1000,6 +1042,9 @@ def np_log_normal_impl2(mean, sigma):
 def lognormal_impl(mean, sigma, size):
     if is_nonelike(size):
         return lambda mean, sigma, size: np.random.lognormal(mean, sigma)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda mean, sigma, size: np.array(np.random.lognormal(mean, sigma))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                             isinstance(size.dtype,
                                                        types.Integer))):
@@ -1050,6 +1095,9 @@ def pareto_impl(a):
 def pareto_impl(a, size):
     if is_nonelike(size):
         return lambda a, size: np.random.pareto(a)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda a, size: np.array(np.random.pareto(a))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                             isinstance(size.dtype,
                                                        types.Integer))):
@@ -1090,6 +1138,9 @@ def weibull_impl(a):
 def weibull_impl2(a, size):
     if is_nonelike(size):
         return lambda a, size: np.random.weibull(a)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda a, size: np.array(np.random.weibull(a))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                             isinstance(size.dtype,
                                                        types.Integer))):
@@ -1162,6 +1213,9 @@ def _vonmisesvariate_impl(_random):
 def vonmises_impl(mu, kappa, size):
     if is_nonelike(size):
         return lambda mu, kappa, size: np.random.vonmises(mu, kappa)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda mu, kappa, size: np.array(np.random.vonmises(mu, kappa))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                             and isinstance(size.dtype,
                                                            types.Integer))):
@@ -1234,6 +1288,10 @@ def binomial_impl(n, p):
 def binomial_impl(n, p, size):
     if is_nonelike(size):
         return lambda n, p, size: np.random.binomial(n, p)
+    if is_empty_tuple(size):
+        # When calling np.random.binomial with size = (), the returned value isn't a
+        # float like when size = None. Instead, it's an array of shape ()
+        return lambda n, p, size: np.array(np.random.binomial(n, p))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                             isinstance(size.dtype,
                                                        types.Integer))):
@@ -1259,6 +1317,8 @@ def chisquare_impl(df):
 def chisquare_impl2(df, size):
     if is_nonelike(size):
         return lambda df, size: np.random.chisquare(df)
+    if is_empty_tuple(size):
+        return lambda df, size: np.array(np.random.chisquare(df))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                             isinstance(size.dtype,
                                                        types.Integer))):
@@ -1288,6 +1348,11 @@ def f_impl(dfnum, dfden, size):
             dfden, (types.Float, types.Integer)) and
        is_nonelike(size)):
         return lambda dfnum, dfden, size: np.random.f(dfnum, dfden)
+    if (isinstance(dfnum, (types.Float, types.Integer)) and isinstance(
+            dfden, (types.Float, types.Integer)) and
+       is_empty_tuple(size)):
+        # Handle size = ()
+        return lambda dfnum, dfden, size: np.array(np.random.f(dfnum, dfden))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                             and isinstance(size.dtype,
                                                            types.Integer))):
@@ -1328,6 +1393,9 @@ def geometric_impl(p):
 def geometric_impl(p, size):
     if is_nonelike(size):
         return lambda p, size: np.random.geometric(p)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda p, size: np.array(np.random.geometric(p))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                             isinstance(size.dtype,
                                                        types.Integer))):
@@ -1355,6 +1423,9 @@ def gumbel_impl(loc, scale):
 def gumbel_impl3(loc, scale, size):
     if is_nonelike(size):
         return lambda loc, scale, size: np.random.gumbel(loc, scale)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda loc, scale, size: np.array(np.random.gumbel(loc, scale))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                             and isinstance(size.dtype,
                                                            types.Integer))):
@@ -1396,6 +1467,10 @@ def hypergeometric_impl(ngood, nbad, nsample, size):
     if is_nonelike(size):
         return lambda ngood, nbad, nsample, size:\
             np.random.hypergeometric(ngood, nbad, nsample)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda ngood, nbad, nsample, size:\
+            np.array(np.random.hypergeometric(ngood, nbad, nsample))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                             and isinstance(size.dtype,
                                                            types.Integer))):
@@ -1430,6 +1505,9 @@ def laplace_impl2(loc, scale):
 def laplace_impl3(loc, scale, size):
     if is_nonelike(size):
         return lambda loc, scale, size: np.random.laplace(loc, scale)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda loc, scale, size: np.array(np.random.laplace(loc, scale))
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                            isinstance(size.dtype,
                                                       types.Integer)):
@@ -1472,6 +1550,9 @@ def logistic_impl2(loc, scale):
 def logistic_impl3(loc, scale, size):
     if is_nonelike(size):
         return lambda loc, scale, size: np.random.logistic(loc, scale)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda loc, scale, size: np.array(np.random.logistic(loc, scale))
     if (isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                             and isinstance(size.dtype,
                                                            types.Integer))):
@@ -1520,6 +1601,8 @@ def logseries_impl(p):
 def logseries_impl(p, size):
     if is_nonelike(size):
         return lambda p, size: np.random.logseries(p)
+    if is_empty_tuple(size):
+        return lambda p, size: np.array(np.random.logseries(p))
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                            isinstance(size.dtype,
                                                       types.Integer)):
@@ -1622,6 +1705,9 @@ def poisson_impl1(lam):
 def poisson_impl2(lam, size):
     if isinstance(lam, (types.Float, types.Integer)) and is_nonelike(size):
         return lambda lam, size: np.random.poisson(lam)
+    if isinstance(lam, (types.Float, types.Integer)) and is_empty_tuple(size):
+        # Handle size = ()
+        return lambda lam, size: np.array(np.random.poisson(lam))
     if isinstance(lam, (types.Float, types.Integer)) and (
             isinstance(size, types.Integer) or
        (isinstance(size, types.UniTuple) and isinstance(size.dtype,
@@ -1652,6 +1738,9 @@ def power_impl(a):
 def power_impl(a, size):
     if is_nonelike(size):
         return lambda a, size: np.random.power(a)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda a, size: np.array(np.random.power(a))
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                            isinstance(size.dtype,
                                                       types.Integer)):
@@ -1685,6 +1774,9 @@ def rayleigh_impl1(scale):
 def rayleigh_impl2(scale, size):
     if is_nonelike(size):
         return lambda scale, size: np.random.rayleigh(scale)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda scale, size: np.array(np.random.rayleigh(scale))
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                            isinstance(size.dtype,
                                                       types.Integer)):
@@ -1709,6 +1801,9 @@ def cauchy_impl():
 def standard_cauchy_impl(size):
     if is_nonelike(size):
         return lambda size: np.random.standard_cauchy()
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda size: np.array(np.random.standard_cauchy())
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                            and isinstance(size.dtype,
                                                           types.Integer)):
@@ -1736,7 +1831,9 @@ def standard_t_impl(df):
 @overload(np.random.standard_t)
 def standard_t_impl2(df, size):
     if is_nonelike(size):
-        return lambda p, size: np.random.standard_t(p)
+        return lambda df, size: np.random.standard_t(df)
+    if is_empty_tuple(size):
+        return lambda df, size: np.array(np.random.standard_t(df))
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                            isinstance(size.dtype,
                                                       types.Integer)):
@@ -1774,6 +1871,9 @@ def wald_impl(mean, scale):
 def wald_impl2(mean, scale, size):
     if is_nonelike(size):
         return lambda mean, scale, size: np.random.wald(mean, scale)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda mean, scale, size: np.array(np.random.wald(mean, scale))
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                            isinstance(size.dtype,
                                                       types.Integer)):
@@ -1809,6 +1909,9 @@ def zipf_impl(a):
 def zipf_impl(a, size):
     if is_nonelike(size):
         return lambda a, size: np.random.zipf(a)
+    if is_empty_tuple(size):
+        # Handle size = ()
+        return lambda a, size: np.array(np.random.zipf(a))
     if isinstance(size, types.Integer) or (isinstance(size, types.UniTuple) and
                                            isinstance(size.dtype,
                                                       types.Integer)):
@@ -2097,7 +2200,7 @@ def dirichlet(alpha, size=None):
             "array or sequence, got %s" % (alpha,)
         )
 
-    if size in (None, types.none):
+    if size in (None, types.none) or is_empty_tuple(size):
 
         def dirichlet_impl(alpha, size=None):
             out = np.empty(len(alpha))
@@ -2177,6 +2280,12 @@ def noncentral_chisquare(df, nonc, size=None):
              validate_noncentral_chisquare_input(df, nonc)
              return noncentral_chisquare_single(df, nonc)
          return noncentral_chisquare_impl
+     if is_empty_tuple(size):
+         # Handle size = ()
+         def noncentral_chisquare_impl(df, nonc, size=None):
+             validate_noncentral_chisquare_input(df, nonc)
+             return np.array(noncentral_chisquare_single(df, nonc))
+         return noncentral_chisquare_impl
      elif isinstance(size, types.Integer) or (isinstance(size, types.UniTuple)
                                               and isinstance(size.dtype,
                                                              types.Integer)):


Test patch:
diff --git a/numba/tests/test_random.py b/numba/tests/test_random.py
index dd6cef2c4..8127ef44d 100644
--- a/numba/tests/test_random.py
+++ b/numba/tests/test_random.py
@@ -531,6 +531,18 @@ class TestRandom(BaseTest):
                        {'low': -2.5, 'high': 1e3},
                        {'low': 1.5, 'high': -2.5}])
 
+    def test_numpy_uniform_empty_size(self):
+        self._check_any_distrib_kwargs(
+            jit_with_kwargs("np.random.uniform", ['low', 'high', 'size']),
+            get_np_state_ptr(),
+            'uniform',
+            paramlist=[{'low': 1.5, 'high': 1e6, 'size': ()},
+                       {'low': -2.5, 'high': 1e3, 'size': ()},
+                       {'low': 1.5, 'high': -2.5, 'size': ()},
+                       {'low': 1.5, 'high': 1e6, 'size': None},
+                       {'low': -2.5, 'high': 1e3, 'size': None},
+                       {'low': 1.5, 'high': -2.5, 'size': None},])
+
     def _check_triangular(self, func2, func3, ptr):
         """
         Check a triangular()-like function.
@@ -1834,6 +1846,60 @@ class TestProcesses(ConcurrencyBaseTest):
 
 
 class TestNumPyRandomAPI(TestCase):
+    API_CALL_TESTS = {"np.random.beta": {'a': 1., 'b': 2., 'size': 3},
+                      "np.random.binomial": {'n': 1, 'p': 0.3, 'size': 3},
+                      "np.random.chisquare": {'df': 2., 'size': 3},
+                      "np.random.choice": {'a': 2, 'size': 3},
+                      "np.random.dirichlet": {'alpha': (2,), 'size': 3},
+                      "np.random.exponential": {'scale': 1., 'size': 3},
+                      "np.random.f": {'dfnum': 1., 'dfden': 2., 'size': 3},
+                      "np.random.gamma": {'shape': 2, 'scale': 2.0, 'size': 3},
+                      "np.random.geometric": {'p': 1., 'size': 3},
+                      "np.random.gumbel": {'loc': 0., 'scale': 1., 'size': 3},
+                      "np.random.hypergeometric": {'ngood': 1, 'nbad': 1,
+                                                   'nsample': 1, 'size': 3},
+                      "np.random.laplace": {'loc': 0., 'scale': 1., 'size': 3},
+                      "np.random.logistic": {'loc': 0., 'scale': 1., 'size': 3},
+                      "np.random.lognormal": {'mean': 0., 'sigma': 1., 'size': 3},
+                      "np.random.logseries": {'p': 0.5, 'size': 3},
+                      "np.random.multinomial": {'n': 1, 'pvals': (1,), 'size': 3},
+                      "np.random.negative_binomial": {'n': 1, 'p': 0.5},
+                      "np.random.noncentral_chisquare": {'df': 1., 'nonc': 1.,
+                                                         'size': 3},
+                      "np.random.normal": {'loc': 0., 'scale': 1., 'size': 3},
+                      "np.random.pareto": {'a': 2., 'size': 3},
+                      # NOTE: The NumPy impl of permutation "takes no keyword
+                      # arguments".
+                      # "np.random.permutation": {'x': (1, 2, 3)},
+                      "np.random.poisson": {'lam': 1., 'size': 3},
+                      "np.random.power": {'a': 2., 'size': 3},
+                      # NOTE: The NumPy impl of rand essentially takes *args so kwargs
+                      # are unsupported.
+                      # "np.random.rand": {'d0': 1, 'd1': 2, ...}}
+                      "np.random.randint": {'low': 1, 'high': 2, 'size': 3},
+                      # NOTE: The NumPy impl of randn essentially takes *args so
+                      # kwargs are unsupported.
+                      # "np.random.randn":  {'d0': 1, 'd1': 2, ...}}
+                      "np.random.random": {'size': 3},
+                      "np.random.random_sample": {'size': 3},
+                      "np.random.ranf": {'size': 3},
+                      "np.random.rayleigh": {'scale': 1., 'size': 3},
+                      "np.random.sample": {'size': 3},
+                      "np.random.seed": {'seed': 4},
+                      # NOTE: The NumPy impl of shuffle "takes no keyword arguments".
+                      # "np.random.shuffle"
+                      "np.random.standard_cauchy": {'size': 3},
+                      "np.random.standard_exponential": {'size': 3},
+                      "np.random.standard_gamma": {'shape': 2., 'size': 3},
+                      "np.random.standard_normal": {'size': 3},
+                      "np.random.standard_t": {'df': 2., 'size': 3},
+                      "np.random.triangular": {'left': 1., 'mode': 2., 'right': 3.,
+                                               'size': 3},
+                      "np.random.uniform": {'low': 1., 'high': 2., 'size': 3},
+                      "np.random.vonmises": {'mu': 1., 'kappa': 2., 'size': 3},
+                      "np.random.wald": {'mean': 1., 'scale': 2., 'size': 3},
+                      "np.random.weibull": {'a': 1., 'size': 3},
+                      "np.random.zipf": {'a': 2., 'size': 3},}
 
     def test_call_by_name(self):
         # Checks that the NumPy impls in Numba can be used via call-by-name
@@ -1844,62 +1910,8 @@ class TestNumPyRandomAPI(TestCase):
 
         # Herein, it doesn't matter what the values are, the names and types
         # just have to make sense.
-        data = {"np.random.beta": {'a': 1., 'b': 2., 'size': 3},
-                "np.random.binomial": {'n': 1, 'p': 0.3, 'size': 3},
-                "np.random.chisquare": {'df': 2., 'size': 3},
-                "np.random.choice": {'a': 2, 'size': 3},
-                "np.random.dirichlet": {'alpha': (2,), 'size': 3},
-                "np.random.exponential": {'scale': 1., 'size': 3},
-                "np.random.f": {'dfnum': 1., 'dfden': 2., 'size': 3},
-                "np.random.gamma": {'shape': 2, 'scale': 2.0, 'size': 3},
-                "np.random.geometric": {'p': 1., 'size': 3},
-                "np.random.gumbel": {'loc': 0., 'scale': 1., 'size': 3},
-                "np.random.hypergeometric": {'ngood': 1, 'nbad': 1,
-                                             'nsample': 1, 'size': 3},
-                "np.random.laplace": {'loc': 0., 'scale': 1., 'size': 3},
-                "np.random.logistic": {'loc': 0., 'scale': 1., 'size': 3},
-                "np.random.lognormal": {'mean': 0., 'sigma': 1., 'size': 3},
-                "np.random.logseries": {'p': 0.5, 'size': 3},
-                "np.random.multinomial": {'n': 1, 'pvals': (1,), 'size': 3},
-                "np.random.negative_binomial": {'n': 1, 'p': 0.5},
-                "np.random.noncentral_chisquare": {'df': 1., 'nonc': 1.,
-                                                   'size': 3},
-                "np.random.normal": {'loc': 0., 'scale': 1., 'size': 3},
-                "np.random.pareto": {'a': 2., 'size': 3},
-                # NOTE: The NumPy impl of permutation "takes no keyword
-                # arguments".
-                # "np.random.permutation": {'x': (1, 2, 3)},
-                "np.random.poisson": {'lam': 1., 'size': 3},
-                "np.random.power": {'a': 2., 'size': 3},
-                # NOTE: The NumPy impl of rand essentially takes *args so kwargs
-                # are unsupported.
-                # "np.random.rand": {'d0': 1, 'd1': 2, ...}}
-                "np.random.randint": {'low': 1, 'high': 2, 'size': 3},
-                # NOTE: The NumPy impl of randn essentially takes *args so
-                # kwargs are unsupported.
-                # "np.random.randn":  {'d0': 1, 'd1': 2, ...}}
-                "np.random.random": {'size': 3},
-                "np.random.random_sample": {'size': 3},
-                "np.random.ranf": {'size': 3},
-                "np.random.rayleigh": {'scale': 1., 'size': 3},
-                "np.random.sample": {'size': 3},
-                "np.random.seed": {'seed': 4},
-                # NOTE: The NumPy impl of shuffle "takes no keyword arguments".
-                # "np.random.shuffle"
-                "np.random.standard_cauchy": {'size': 3},
-                "np.random.standard_exponential": {'size': 3},
-                "np.random.standard_gamma": {'shape': 2., 'size': 3},
-                "np.random.standard_normal": {'size': 3},
-                "np.random.standard_t": {'df': 2., 'size': 3},
-                "np.random.triangular": {'left': 1., 'mode': 2., 'right': 3.,
-                                         'size': 3},
-                "np.random.uniform": {'low': 1., 'high': 2., 'size': 3},
-                "np.random.vonmises": {'mu': 1., 'kappa': 2., 'size': 3},
-                "np.random.wald": {'mean': 1., 'scale': 2., 'size': 3},
-                "np.random.weibull": {'a': 1., 'size': 3},
-                "np.random.zipf": {'a': 2., 'size': 3},}
-
-        for fn, args in data.items():
+        
+        for fn, args in self.API_CALL_TESTS.items():
             argstr = ', '.join([f'{k}={v}' for k, v in args.items()])
             template = dedent(f"""
                 def foo():
@@ -1915,6 +1927,39 @@ class TestNumPyRandomAPI(TestCase):
             func()
             njit(func).compile(())
 
+    def test_call_distributions_with_empty_size(self):
+        # Check that the distributions can be called with size=() as per the
+        # NumPy API, see issue numba#8975.
+        for fn, args in self.API_CALL_TESTS.items():
+            if 'size' not in args:
+                continue
+            
+            args['size'] = ()
+            argstr = ', '.join([f'{k}={v}' for k, v in args.items()])
+            template = dedent(f"""
+                def foo():
+                    return {fn}({argstr})
+                """)
+            l = {}
+            exec(template, {'np': np}, l)
+
+            # Store result obtained by calling the np and nb functions.
+            # We are not interested by the values, they were tested above
+            # but we want to ensure that the function compiles, and that 
+            # the shapes and types are the same between numpy and numba.
+            func = l['foo']
+            np_val = func()
+
+            nb_func = njit(func).compile(())
+            nb_val = nb_func()
+
+            if isinstance(nb_val, np.ndarray):
+                self.assertEqual(nb_val.shape, np_val.shape)
+            else:
+                self.assertIsInstance(nb_val, np_val.__class__)
+
+
+
 
 if __name__ == "__main__":
     unittest.main()