Home Tutorials Standard Library

Standard Library

Python's math Module: Common Functions Beyond the Basic Operators

Pyford Notes July 6, 2026 7 min read
Key points
  • math works with plain float/int values only — complex number support lives in the separate cmath module.
  • math.floor(), math.ceil(), and math.trunc() agree for positive numbers but diverge for negative ones, which is a common source of off-by-one bugs.
  • math.sqrt(-1) raises ValueError rather than returning nan or a complex number — a deliberate design choice, not an oversight.
  • math.isclose() exists because == on floats fails for values that are mathematically equal but differ in the last bit or two of precision.

Constants: pi, e, inf, and nan

math exposes a small set of constants as plain module attributes:

import math

print(math.pi)    # 3.141592653589793
print(math.e)      # 2.718281828459045
print(math.inf)    # positive infinity, a float
print(math.nan)    # "not a number", also a float

math.inf is genuinely useful as a starting value when searching for a minimum (best = math.inf before a loop that only ever lowers it), avoiding a separate "have we seen anything yet" flag. math.nan is trickier: it compares unequal to everything, including itself, so x == math.nan is always False even when x is genuinely NaN — use math.isnan(x) to actually test for it.

floor, ceil, trunc, and how they differ

All three round to an integer, but they disagree on negative numbers because they round in different directions rather than toward or away from zero consistently:

math.floor(2.7)    # 2   (toward negative infinity)
math.floor(-2.7)   # -3
math.ceil(2.3)      # 3   (toward positive infinity)
math.ceil(-2.3)     # -2
math.trunc(2.7)     # 2   (toward zero)
math.trunc(-2.7)    # -2

The built-in round() is a fourth, different behaviour again: it rounds to the nearest value and uses banker's rounding on exact ties (round(2.5) is 2, not 3). Picking the wrong one of these four for a billing calculation or a pagination count (how many pages of 20 items are needed is math.ceil(count / 20), not round()) is an easy, quiet bug.

Power and roots: sqrt vs **0.5

math.sqrt(x) and x ** 0.5 give the same result for positive x, but they fail differently on negative input:

math.sqrt(-4)     # ValueError: math domain error
(-4) ** 0.5        # (1.2246...e-16+2j) -- a complex number

**0.5 silently switches to complex arithmetic for a negative base, which is rarely what a caller expecting a real-valued measurement wants — a bug that produces a complex number three calculations downstream is much harder to trace than one that raises immediately. math.sqrt() failing loudly and immediately is usually the safer choice unless complex results are genuinely expected, in which case cmath.sqrt() is the explicit tool for that. math.pow(x, y) also exists and always returns a float, unlike ** which keeps int results as int when both operands are integers.

Comparing floats safely with isclose

Floating-point arithmetic accumulates tiny representation errors, so two values that are mathematically identical often aren't bit-for-bit equal after a chain of operations:

0.1 + 0.2 == 0.3          # False
math.isclose(0.1 + 0.2, 0.3)   # True

isclose() takes optional rel_tol and abs_tol keyword arguments to control how close is "close enough" — the default relative tolerance (about 1e-9) is fine for general use but too loose or too tight depending on the magnitudes involved, so it's worth reading the docs on both parameters before relying on the defaults in anything measuring very small or very large numbers. This is a companion problem to the exact-arithmetic guarantees the decimal and fractions modules provide when floating-point error isn't acceptable at all, such as in financial calculations.

gcd, factorial, and logarithms

A handful of number-theory and combinatorics helpers round out the module:

math.gcd(48, 18)     # 6
math.lcm(4, 6)         # 12  (Python 3.9+)
math.factorial(6)      # 720
math.log(100, 10)      # 2.0 -- log base 10
math.log2(8)            # 3.0

math.factorial() raises ValueError on negative input rather than returning something nonsensical, and grows fast enough that math.factorial(1000) is already a genuinely large integer — Python's arbitrary-precision integers handle it without overflow, but the result can be slow to print or serialize. math.log(x, base) with an explicit base is more numerically accurate than manually dividing by math.log(base), since the two-argument form is computed with a dedicated algorithm rather than composed from two separate logarithms.