math Module Complexity¶
The math module provides fast floating-point math functions and a few integer-specific helpers.
Complexity Reference¶
| Operation | Time | Space | Notes |
|---|---|---|---|
math.pi, math.e, math.tau, math.inf, math.nan |
O(1) | O(1) | Module constants; no call is made |
math.sin/cos/tan(x), math.asin/acos/atan(x), math.atan2(y, x) |
O(1) | O(1) | One libm call on a fixed-width operand |
math.sinh/cosh/tanh(x), math.asinh/acosh/atanh(x) |
O(1) | O(1) | One libm call |
math.exp(x), math.exp2(x), math.expm1(x), math.pow(x, y) |
O(1) | O(1) | exp2 is 3.11+; math.pow always returns a float and overflows where the builtin pow returns an exact int |
math.log(x[, base]), math.log2(x), math.log10(x), math.log1p(x) |
O(1) | O(1) | All but log1p take a separate path for an int too large for a float, costing O(n) in its digits |
math.sqrt(x), math.cbrt(x) |
O(1) | O(1) | cbrt is 3.11+ |
math.erf/erfc(x), math.gamma/lgamma(x) |
O(1) | O(1) | One libm call |
math.degrees/radians(x), math.fabs(x), math.copysign(x, y) |
O(1) | O(1) | |
math.fmod(x, y), math.remainder(x, y), math.modf(x), math.frexp(x), math.ldexp(x, i) |
O(1) | O(1) | |
math.nextafter(x, y), math.ulp(x), math.fma(x, y, z) |
O(1) | O(1) | fma is 3.13+ |
math.isfinite/isinf/isnan(x), math.isclose(a, b) |
O(1) | O(1) | These convert an int argument too, so a value beyond the float range raises rather than answering |
math.floor(x), math.ceil(x), math.trunc(x) |
O(1) | O(1) | An int argument is returned unchanged, however many digits it has |
math.hypot(*coords) |
O(n) | O(n) | n = number of coordinates; they are buffered, not folded in one at a time |
math.dist(p, q) |
O(n) | O(n) | n = dimensions; buffered like hypot |
math.fsum(iterable) |
O(n) | O(1) | n = values; the partial sums it carries are bounded by the float exponent range, not by n |
math.sumprod(p, q) |
O(n) | O(1) | Python 3.12+; unlike dist, it holds nothing per element |
math.prod(iterable) |
O(n) | O(1) | n = values; with int values the product grows, making it O(d²) time and O(d) space in the result's digits |
math.isqrt(n) |
O(n²) | O(n) | n = digits of the argument |
math.gcd(*integers) |
O(n²) | O(n) | n = digits of the smaller operand |
math.lcm(*integers) |
O(n²) | O(n) | n = digits; the gcd dominates, and the result carries up to the operands' digits combined |
math.factorial(n) |
O(d²) | O(d) | d = digits of the result, itself Θ(n log n) bits |
math.comb(n, k) |
O(d²) | O(d) | d = digits of the result; driven by min(k, n - k), barely by n |
math.perm(n, k) |
O(d²) | O(d) | d = digits of the result; driven by k, with no comb symmetry to fall back on |
Floating-point precision
Most math functions operate on binary floating-point numbers. Results are fast but may be imprecise
for some decimal fractions. Use decimal for exact decimal arithmetic.
Common Operations¶
Basic Transcendentals¶
import math
x = 2.0
# O(1)
root = math.sqrt(x)
log2 = math.log(x, 2)
angle = math.sin(math.pi / 6)
Integer Arguments to the Float Functions¶
Every function in the rows above takes a C double. An int argument is
converted first, which costs O(n) in its digits and raises OverflowError once
the value exceeds the float range - the predicates included, so
math.isfinite() raises on such a value rather than answering.
Two groups are exempt:
floor(),ceil()andtrunc()return an int argument unchanged, at O(1) whatever its sizelog(),log2()andlog10()have a large-int path of their own, costing O(n) in the argument's digits
import math
big = (1 << 2000) + 1
# A path of its own: O(n) in the digits of `big`
math.log(big) # 1386.29...
# Returned unchanged, at O(1)
math.floor(big) is big # True
# Converted, and no float can hold it
for name in ('sqrt', 'exp', 'isfinite'):
try:
getattr(math, name)(big)
except OverflowError as error:
print(name, '->', error)
Which of the Sequence Functions Hold Their Input¶
Four functions walk a sequence of floats, and they divide on space rather than
on time. hypot() and dist() hold the coordinates: their peak grows with the
input. fsum() and sumprod() do not.
import math
# O(n) time and O(n) space in the number of coordinates
r2 = math.hypot(3.0, 4.0) # 5.0
r3 = math.hypot(1.0, 2.0, 2.0) # 3.0
d = math.dist((0.0, 0.0), (3.0, 4.0)) # 5.0
# O(n) time, O(1) space - neither holds anything per element
total = math.fsum([0.1] * 10) # 1.0
product = math.prod([2.0, 3.0, 7.0]) # 42.0
Where a dot product is what you want, sumprod() (3.12+) is the O(1)-space
member of the family; dist() is the one that buffers.
Accurate Summation¶
fsum() returns the correctly rounded sum of its input. sum() does not, but
it is much closer than it once was: from Python 3.12 it carries a compensation
term when adding floats, which is enough for the textbook example.
import math
values = [1e100, 1.0, -1e100]
# Loses the 1.0 entirely before 3.12; from 3.12 it does not
regular = sum(values)
# fsum has always been exact here
accurate = math.fsum(values) # 1.0
One compensation term is not exactness, though. Three magnitudes at once still separate them on every supported version:
import math
values = [1e16, 1.0, 1e-16, -1e16, -1.0]
sum(values) # -1.0 before 3.12, 0.0 from 3.12 - wrong either way
math.fsum(values) # 1e-16, the correctly rounded answer
fsum() is O(1) in space however long the iterable is. It carries a set of
non-overlapping partial sums, and a float's exponent range caps how many of
those can exist at once, so the working set does not grow with the input.
Integers: gcd, lcm, factorial, comb, perm¶
import math
# gcd/lcm: O(n²) in the digits of the smaller operand
g = math.gcd(84, 30) # 6
l = math.lcm(6, 10) # 30
# Both take any number of arguments
g3 = math.gcd(12, 18, 30) # 6
l3 = math.lcm(4, 6, 10) # 60
# factorial, combinations, permutations: O(d²) in the digits of the result
f = math.factorial(10) # 3628800
c = math.comb(10, 3) # 120
p = math.perm(10, 3) # 720
# isqrt: O(n²) in the digits of the argument, and exact where sqrt() is not
r = math.isqrt(10**40) # 100000000000000000000
prod() sits in both camps. Over floats it is O(n) time and O(1) space, like
fsum(). Over ints the running product grows with every factor, so the cost
follows the digits of the result rather than the number of terms:
import math
# O(n) over floats, holding nothing
math.prod([1.5] * 1000)
# Over ints the product itself is the cost: 2000 factors, a 3170-bit result
math.prod([3] * 2000) # O(d²) in the digits of the result
What Drives comb() and perm()¶
n is barely a cost variable for either: its only effect is the digit count of
the individual factors, which grows as log n. k is the one that matters, and
the two functions differ in what they can do about it.
comb(n, k) == comb(n, n - k), and a k near n costs what the small k it
mirrors costs. perm() has no such identity, so the two diverge sharply as
k approaches n:
import math
# comb: mirrored, so both of these are equally cheap
math.comb(1000, 5) # 8250291250200
math.comb(1000, 995) # the same value, and the same work
# perm: k factors, so a large k really does cost more
math.perm(1000, 5) # 990034950024000
Robust Comparisons¶
import math
# Avoid direct equality for floats
math.isclose(0.1 + 0.2, 0.3) # True
Performance Notes¶
- Floating-point operations are constant time and CPU-bound.
- The integer functions (
factorial,comb,perm,gcd,lcm) scale with the digit counts in the table, not with the numeric value of their arguments. The quadratic bounds are grade-school ones; CPython's Karatsuba multiplication beats them for large operands.
Version Notes¶
- Python 3.9+:
math.lcm()added, andmath.gcd()became variadic - Python 3.11+:
math.cbrt()andmath.exp2()added - Python 3.12+:
math.sumprod()added; the builtinsum()compensates when adding floats, which narrows the accuracy gap tofsum()without closing it - Python 3.13+:
math.fma()added
Related Modules¶
- cmath Module - Complex-number math
- decimal Module - Decimal arithmetic
- fractions Module - Rational arithmetic
- statistics Module - Descriptive statistics
- random Module - Random numbers