Accepting floats and coercing them to int is not like any other math functions. There is a huge difference between an approximate floating point calculation with bounded cost vs an exact calculation with unbounded cost regardless of what types are accepted and coerced as inputs. The nature of the internal calculation and the cost vs output accuracy guarantees are the reason for choosing to use say scipy.factorial(n) or mpmath.factorial(n) over math.factorial(n):
In [5]: %time scipy.special.factorial(10_000_000)
CPU times: user 117 μs, sys: 11 μs, total: 128 μs
Wall time: 133 μs
Out[5]: np.float64(inf)
In [6]: %time mpmath.factorial(10_000_000)
CPU times: user 1.28 ms, sys: 0 ns, total: 1.28 ms
Wall time: 1.3 ms
Out[6]: mpf('1.2024234005159035e+65657059')
In [7]: %time out = math.factorial(10_000_000)
CPU times: user 8min 48s, sys: 1.16 s, total: 8min 49s
Wall time: 8min 47s
In [8]: out.bit_length() / (8*1024**2)
Out[8]: 26.000503301620483
The output from scipy here is guaranteed to be a 64 bit float so fixed size but that means we get inf in 0.1 ms. The output from mpmath has unbounded exponent but is computed to fixed precision mantissa and gives us an accurate but approximate value in 1.3 ms. The output from math.factorial is a 26 megabyte integer that took almost 10 minutes to compute on this computer. If I increase n then math.factorial will happily consume all available memory and crash the entire computer but first it will lock up the CPU for longer than I am prepared to wait for this demonstration.
Before these integer functions were added the math module only had functions like the scipy one that you can think of as being O(1) computational cost without getting into trouble. Coercing a float into an int and then performing unbounded computation and producing outputs of unbounded size is a completely different kind of operation regardless of whether floats are allowed as input. There are good reasons for using something like scipy.factorial by default over something like math.factorial in most cases.
While mpmath handles the case above efficiently it can still get you into trouble if you go one level up in size because it does not bound the exponent:
In [12]: %time out = mpmath.factorial(mpmath.factorial(10_000))
CPU times: user 71.3 ms, sys: 3.99 ms, total: 75.3 ms
Wall time: 73.6 ms
In [13]: %time out = mpmath.factorial(mpmath.factorial(100_000))
CPU times: user 1.13 s, sys: 0 ns, total: 1.13 s
Wall time: 1.13 s
In [14]: %time out = mpmath.factorial(mpmath.factorial(1_000_000))
^C^C---------------------------------------------------------------------------
KeyboardInterrupt
There is a huge difference between an operation that is bounded in time and space and an operation that is not and there are not good reasons to mix up the kind of exact calculation that math.factorial does with approximate fixed precision floating point calculations.
An imath module can come with a big banner clearly stating that these operations are unbounded. Ideally it would also provide contexts that can be used to bound them making them safe to use for potentially large inputs:
from imath import context
ctx = context(maxbits=1000)
ctx.factorial(1000) # OverflowError