ZetCode

Python math.integer

last modified October 29, 2026 Python 3.15

In this article we show how to use the math.integer module, which was added in Python 3.15 by PEP 791. We also cover the new floating-point functions that Python 3.15 added to the math module.

The math module has two kinds of functions. Most of them, such as sqrt, sin, or log, work with floating-point numbers and return floats. A few work only with integers and return exact integer results. The math.integer module collects the integer functions in one place:

The functions with the same names in the math module remain as aliases for backward compatibility. Since Python 3.15 they are soft deprecated: they produce no warning and are not scheduled for removal, but new code should use math.integer.

The module contents

The first example lists the public functions of the module.

contents.py
import math
import math.integer

names = [name for name in dir(math.integer) if not name.startswith('_')]
print(names)

print(math.integer.isqrt(17))
print(math.integer.gcd is math.gcd)

The module is imported with import math.integer; the functions can also be imported directly with from math.integer import gcd, which we do in the rest of the article.

print(math.integer.gcd is math.gcd)

The aliases in math are the very same function objects, so both names behave identically.

$ python contents.py
['comb', 'factorial', 'gcd', 'isqrt', 'lcm', 'perm']
4
True

Greatest common divisor

The gcd function returns the greatest common divisor of its arguments: the largest integer that divides all of them without a remainder.

gcd_fraction.py
from math.integer import gcd

print(gcd(48, 18))
print(gcd(48, 18, 30))
print(gcd(-12, 8))
print(gcd(0, 7))
print(gcd())

num, den = 1071, 462
d = gcd(num, den)
print(f'{num}/{den} = {num // d}/{den // d}')

The function accepts any number of arguments.

print(gcd(-12, 8))
print(gcd(0, 7))
print(gcd())

The result is always non-negative. Zero is divisible by every integer, so gcd(0, 7) is 7. Called without arguments, the function returns 0.

num, den = 1071, 462
d = gcd(num, den)
print(f'{num}/{den} = {num // d}/{den // d}')

Dividing the numerator and the denominator of a fraction by their greatest common divisor reduces the fraction to lowest terms.

$ python gcd_fraction.py
6
6
4
7
0
1071/462 = 51/22

Least common multiple

The lcm function returns the least common multiple: the smallest positive integer that is divisible by all the arguments.

lcm_schedule.py
from math.integer import lcm

print(lcm(4, 6))
print(lcm(4, 6, 10))
print(lcm(5, 0))
print(lcm())

intervals = {'line 3': 12, 'line 7': 18, 'line 11': 30}
minutes = lcm(*intervals.values())
print(f'All three buses leave together again after {minutes} minutes')

If any argument is zero, the result is zero. Without arguments, the function returns 1.

intervals = {'line 3': 12, 'line 7': 18, 'line 11': 30}
minutes = lcm(*intervals.values())

Three bus lines leave a stop at the same time, every 12, 18, and 30 minutes. The least common multiple of the intervals tells us when all three leave together again.

$ python lcm_schedule.py
12
60
0
1
All three buses leave together again after 180 minutes

Integer square root

The isqrt function returns the integer part of the square root of a non-negative integer, that is, the largest integer a such that a * a <= n.

isqrt_demo.py
import math
from math.integer import isqrt

print(isqrt(17), isqrt(25), isqrt(0))

n = 2**106 - 1
print(int(math.sqrt(n)))
print(isqrt(n))


def is_square(n):
    return isqrt(n) ** 2 == n


def ceil_sqrt(n):
    return 1 + isqrt(n - 1) if n else 0


print([k for k in range(1, 100) if is_square(k)])
print(ceil_sqrt(16), ceil_sqrt(17))

The result is computed exactly, without converting the number to a float.

n = 2**106 - 1
print(int(math.sqrt(n)))
print(isqrt(n))

For large numbers, the float-based math.sqrt loses precision. A float has 53 bits of mantissa, so 2**106 - 1 is rounded up to 2**106 before the root is computed, and the result is off by one. The isqrt function works with integers of any size.

def is_square(n):
    return isqrt(n) ** 2 == n


def ceil_sqrt(n):
    return 1 + isqrt(n - 1) if n else 0

Two common helpers: a perfect-square test and the ceiling of the square root, the smallest integer a such that a * a >= n.

$ python isqrt_demo.py
4 5 0
9007199254740992
9007199254740991
[1, 4, 9, 16, 25, 36, 49, 64, 81]
4 5

Primality test

A number is prime if it has no divisor between 2 and its square root. The isqrt function gives the upper bound of the search.

primes.py
from math.integer import isqrt


def is_prime(n):
    if n < 2:
        return False
    return all(n % d for d in range(2, isqrt(n) + 1))


print([n for n in range(50) if is_prime(n)])
print(is_prime(2**31 - 1))
print(is_prime(2**31 + 1))

The all function returns False as soon as some n % d is zero, that is, as soon as a divisor is found.

return all(n % d for d in range(2, isqrt(n) + 1))

For the Mersenne prime 2**31 - 1, only divisors up to 46,340 have to be tried. The number 2**31 + 1 is divisible by 3.

$ python primes.py
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
True
False

Factorial

The factorial function returns the product of all positive integers up to n. By definition, factorial(0) is 1.

factorial_demo.py
from math.integer import factorial

print([factorial(n) for n in range(8)])

f = factorial(100)
digits = str(f)
print(len(digits))


def trailing_zeros(n):
    count = 0
    while n:
        n //= 5
        count += n
    return count


print(trailing_zeros(100))
print(len(digits) - len(digits.rstrip('0')))

Python integers have arbitrary precision, so the factorial of 100 is computed exactly; it has 158 digits.

def trailing_zeros(n):
    count = 0
    while n:
        n //= 5
        count += n
    return count

The number of trailing zeros of n! equals the number of factors of 5 in the product, which Legendre's formula counts without computing the factorial. We verify the result by stripping the zeros from the digits.

$ python factorial_demo.py
[1, 1, 2, 6, 24, 120, 720, 5040]
158
24
24

Combinations and permutations

The comb(n, k) function returns the number of ways to choose k items from n items when the order does not matter. The perm(n, k) function returns the number of ways when the order matters.

comb_perm.py
from math.integer import comb, factorial, perm

print(comb(5, 2), perm(5, 2))
print(comb(5, 7), perm(5, 7))
print(perm(5), factorial(5))

print(f'Ways to pick 6 numbers out of 49: {comb(49, 6):,}')
print(f'Possible podiums with 10 runners: {perm(10, 3):,}')

Choosing two items out of five gives 10 combinations and 20 permutations, since each pair can be ordered in two ways.

print(comb(5, 7), perm(5, 7))
print(perm(5), factorial(5))

When k is greater than n, both functions return 0. If k is omitted, perm returns n!, the number of orderings of all n items.

print(f'Ways to pick 6 numbers out of 49: {comb(49, 6):,}')
print(f'Possible podiums with 10 runners: {perm(10, 3):,}')

A lottery ticket is a combination, since the order of the drawn numbers does not matter. A podium is a permutation, because first, second, and third place are different results.

$ python comb_perm.py
10 20
0 0
120 120
Ways to pick 6 numbers out of 49: 13,983,816
Possible podiums with 10 runners: 720

Pascal's triangle

The entries of Pascal's triangle are binomial coefficients: the k-th entry of row n is comb(n, k).

pascal.py
from math.integer import comb

rows = 7
width = rows * 4

for n in range(rows):
    line = ' '.join(f'{comb(n, k):3}' for k in range(n + 1))
    print(line.center(width).rstrip())

Each row is built from the binomial coefficients and centered with the center string method.

$ python pascal.py
              1
            1   1
          1   2   1
        1   3   3   1
      1   4   6   4   1
    1   5  10  10   5   1
  1   6  15  20  15   6   1

Poker hand probabilities

The comb function is the basic tool for counting in probability. The next example computes the odds of several poker hands.

poker.py
from math.integer import comb

hands = comb(52, 5)

counts = {
    'four of a kind': 13 * comb(4, 4) * comb(48, 1),
    'full house': 13 * comb(4, 3) * 12 * comb(4, 2),
    'flush (incl. straight flush)': comb(4, 1) * comb(13, 5),
    'three of a kind': 13 * comb(4, 3) * comb(12, 2) * 4 ** 2,
    'two pair': comb(13, 2) * comb(4, 2) ** 2 * 11 * 4,
}

print(f'{hands:,} possible hands')

for name, count in counts.items():
    print(f'{name:30} {count:8,}  1 in {hands / count:,.0f}')

There are comb(52, 5) five-card hands. Each count multiplies the number of ways to pick ranks by the number of ways to pick suits.

'full house': 13 * comb(4, 3) * 12 * comb(4, 2),

For example, a full house consists of three cards of one rank (13 ranks, 4 choose 3 suits) and two cards of another rank (the 12 remaining ranks, 4 choose 2 suits).

$ python poker.py
2,598,960 possible hands
four of a kind                      624  1 in 4,165
full house                        3,744  1 in 694
flush (incl. straight flush)      5,148  1 in 505
three of a kind                  54,912  1 in 47
two pair                        123,552  1 in 21

Invalid arguments

The functions raise a ValueError for negative arguments where they make no sense and a TypeError for non-integers.

arg_errors.py
from math.integer import comb, factorial, gcd, isqrt

calls = [
    ('factorial(-1)', lambda: factorial(-1)),
    ('factorial(5.0)', lambda: factorial(5.0)),
    ('isqrt(-4)', lambda: isqrt(-4)),
    ('comb(5, -1)', lambda: comb(5, -1)),
    ('gcd(4.0, 6)', lambda: gcd(4.0, 6)),
]

for label, call in calls:
    try:
        call()
    except (ValueError, TypeError) as e:
        print(f'{label:15} {type(e).__name__}: {e}')

Each call is wrapped in a lambda so that we can catch and print its exception. Note that factorial(5.0) is rejected even though 5.0 has an integral value; floats are never accepted.

$ python arg_errors.py
factorial(-1)   ValueError: factorial() not defined for negative values
factorial(5.0)  TypeError: 'float' object cannot be interpreted as an integer
isqrt(-4)       ValueError: isqrt() argument must be nonnegative
comb(5, -1)     ValueError: k must be a non-negative integer
gcd(4.0, 6)     TypeError: 'float' object cannot be interpreted as an integer

The __index__ method

The functions accept integers and any objects that implement the __index__ special method.

index_protocol.py
from math.integer import factorial, gcd


class Count:

    def __init__(self, value):
        self.value = value

    def __index__(self):
        return self.value


print(factorial(Count(6)))
print(gcd(Count(12), 18))
print(factorial(True))

The __index__ method tells Python how to convert an object to an integer losslessly. It is also used by slicing and by functions such as bin and hex.

print(factorial(True))

The bool type is a subclass of int, so True is accepted as 1.

$ python index_protocol.py
720
6
1

The signbit function

Python 3.15 also added several floating-point functions to the math module. The math.signbit function returns True if the sign bit of a float is set.

signbit.py
import math

values = [3.0, -3.0, 0.0, -0.0, math.inf, -math.inf, math.nan, -math.nan]

for x in values:
    print(f'{x!r:>5}   x < 0: {x < 0!s:5}   signbit: {math.signbit(x)}')

For most values, signbit(x) gives the same answer as x < 0. The two differ for negative zero and NaN. Negative zero compares equal to zero, so -0.0 < 0 is false, but its sign bit is set. NaN values also carry a sign bit, although their representation does not show it. Before Python 3.15, the sign bit was usually tested with math.copysign(1, x) < 0.

$ python signbit.py
  3.0   x < 0: False   signbit: False
 -3.0   x < 0: True    signbit: True
  0.0   x < 0: False   signbit: False
 -0.0   x < 0: False   signbit: True
  inf   x < 0: False   signbit: False
 -inf   x < 0: True    signbit: True
  nan   x < 0: False   signbit: False
  nan   x < 0: False   signbit: True

The fmax and fmin functions

The math.fmax and math.fmin functions return the larger and the smaller of two floats. Unlike the built-in max and min, they treat NaN as missing data.

fmax_fmin.py
import math
from functools import reduce

print(max(21.5, math.nan), max(math.nan, 21.5))
print(math.fmax(21.5, math.nan), math.fmax(math.nan, 21.5))
print(math.fmin(math.nan, math.nan))

readings = [math.nan, 21.5, 23.0, math.nan, 19.8]

print(max(readings), min(readings))
print(reduce(math.fmax, readings), reduce(math.fmin, readings))

Every comparison with NaN is false, so the result of the built-in max depends on the order of the arguments.

print(math.fmax(21.5, math.nan), math.fmax(math.nan, 21.5))
print(math.fmin(math.nan, math.nan))

The fmax function ignores a NaN argument and returns the other value. It returns NaN only if both arguments are NaN.

print(reduce(math.fmax, readings), reduce(math.fmin, readings))

The functions take exactly two arguments. To find the extremes of a sequence with missing measurements, we fold the sequence with functools.reduce. The built-in functions return NaN here, because the first reading is NaN.

$ python fmax_fmin.py
21.5 nan
21.5 21.5
nan
nan nan
23.0 19.8

Normal and subnormal numbers

The math.isnormal and math.issubnormal functions classify floats. A normal float uses the full 53-bit precision. Numbers smaller than sys.float_info.min can still be represented as subnormal floats, but with fewer significant bits.

subnormal.py
import math
import sys

smallest_normal = sys.float_info.min

values = [1.0, smallest_normal, smallest_normal / 2, 5e-324,
          0.0, math.inf, math.nan]

for x in values:
    print(f'{x!r:>23}   normal: {math.isnormal(x)!s:5}   '
          f'subnormal: {math.issubnormal(x)}')

The smallest normal float is about 2.2e-308. Half of it is subnormal, and 5e-324 is the smallest positive subnormal float. Zero, infinity, and NaN are neither normal nor subnormal. Subnormal numbers lose precision and can be slow on some hardware, so numerical code sometimes checks for them.

$ python subnormal.py
                    1.0   normal: True    subnormal: False
2.2250738585072014e-308   normal: True    subnormal: False
1.1125369292536007e-308   normal: False   subnormal: True
                 5e-324   normal: False   subnormal: True
                    0.0   normal: False   subnormal: False
                    inf   normal: False   subnormal: False
                    nan   normal: False   subnormal: False

Source

math.integer – integer-specific mathematics functions

math – mathematical functions

PEP 791 – math.integer

In this article we have worked with the math.integer module and the new floating-point functions of the math module in Python 3.15.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.