Numbers: math, random, statistics, decimal and fractions

Five standard library modules cover every numeric need core Python has: exact maths, randomness, summary statistics, exact decimals and exact fractions.

What the standard library is

Python ships with a large collection of modules that are always available, with no installation step. This is the "batteries included" principle, and everything in this path stays inside it - there are no third party packages anywhere in these notes.

A standard library module still needs importing. It is not built in the way len and print are; it simply comes with Python.

math

import math

print(math.pi)              # 3.141592653589793
print(math.e)               # 2.718281828459045
print(math.inf, -math.inf)
print(math.tau)             # 2 * pi

Rounding

print(math.floor(3.7), math.floor(-3.2))     # 3 -4   always down
print(math.ceil(3.2), math.ceil(-3.7))       # 4 -3   always up
print(math.trunc(3.7), math.trunc(-3.7))     # 3 -3   towards zero
print(round(3.7), round(-3.7))               # 4 -4   to nearest, half to even
Function3.7-3.7Direction
floor3-4Down
ceil4-3Up
trunc3-3Towards zero
round4-4Nearest, ties to even

Powers, roots and logarithms

print(math.sqrt(16))            # 4.0
print(math.isqrt(17))           # 4 - integer square root, exact
print(math.pow(2, 10))          # 1024.0 - always a float
print(2 ** 10)                  # 1024 - stays an int
print(math.log(math.e))         # 1.0 - natural log
print(math.log(1000, 10))       # 2.9999999999999996 - floating point
print(math.log10(1000))         # 3.0 - use the dedicated function
print(math.log2(1024))          # 10.0
print(math.exp(1))              # e

Number theory

print(math.gcd(48, 18))         # 6
print(math.lcm(4, 6))           # 12   (Python 3.9+)
print(math.factorial(5))        # 120
print(math.comb(5, 2))          # 10 - combinations
print(math.perm(5, 2))          # 20 - permutations

Checks and comparisons

print(math.isclose(0.1 + 0.2, 0.3))              # True
print(math.isclose(1000, 1001, rel_tol=0.01))    # True - within 1 percent
print(math.isnan(float("nan")))                  # True
print(math.isfinite(math.inf))                   # False
print(math.fsum([0.1] * 10))                     # 1.0 exactly
print(sum([0.1] * 10))                           # 0.9999999999999999

math.fsum adds floats without accumulating rounding error. Use it when summing a long list of decimals.

Trigonometry and distance

print(math.degrees(math.pi))         # 180.0
print(math.radians(180))             # 3.141592653589793
print(round(math.sin(math.radians(30)), 4))     # 0.5
print(math.hypot(3, 4))              # 5.0
print(math.dist((0, 0), (3, 4)))     # 5.0 - any number of dimensions

random

import random

print(random.random())              # a float in [0.0, 1.0)
print(random.uniform(1, 10))        # a float in [1, 10]
print(random.randint(1, 6))         # an int in [1, 6] - BOTH ends included
print(random.randrange(1, 7))       # an int in [1, 7) - like range
print(random.randrange(0, 100, 5))  # a multiple of 5 below 100
randint(a, b) includes b; randrange(a, b) excludes it. This is the one inconsistency in the module and it is a reliable source of off by one errors.

Choosing from a sequence

colours = ["red", "green", "blue", "yellow"]

print(random.choice(colours))               # one item
print(random.choices(colours, k=3))         # 3 items, WITH replacement
print(random.sample(colours, k=3))          # 3 items, WITHOUT replacement

print(random.choices(colours, weights=[10, 1, 1, 1], k=5))    # red is likelier

deck = list(range(1, 11))
random.shuffle(deck)                        # shuffles IN PLACE, returns None
print(deck)

Reproducible randomness

random.seed(42)
print([random.randint(1, 100) for _ in range(5)])

random.seed(42)
print([random.randint(1, 100) for _ in range(5)])     # exactly the same

Seeding makes a sequence repeatable, which is essential for tests and simulations you need to reproduce.

Never use random for security

import secrets

print(secrets.token_hex(16))         # a secure random token
print(secrets.token_urlsafe(16))
print(secrets.randbelow(100))
print(secrets.choice(["a", "b", "c"]))

random uses a fast generator whose output is predictable if enough values are observed. For passwords, tokens, session ids or anything an attacker should not guess, use secrets.

statistics

import statistics

scores = [72, 85, 90, 85, 64, 78]

print(statistics.mean(scores))          # 79.0
print(statistics.median(scores))        # 81.5
print(statistics.mode(scores))          # 85 - the most common value
print(statistics.stdev(scores))         # sample standard deviation
print(statistics.pstdev(scores))        # population standard deviation
print(statistics.variance(scores))
print(statistics.quantiles(scores, n=4))     # the quartiles

print(statistics.fmean(scores))         # faster, always returns a float
print(statistics.median_low([1, 2, 3, 4]))   # 2
print(statistics.median_high([1, 2, 3, 4]))  # 3
from statistics import multimode

print(multimode([1, 1, 2, 2, 3]))       # [1, 2] - mode() would raise on a tie
# statistics.mean([])                   # StatisticsError on empty input

decimal

from decimal import Decimal, getcontext, ROUND_HALF_UP

print(0.1 + 0.2)                                  # 0.30000000000000004
print(Decimal("0.1") + Decimal("0.2"))            # 0.3
print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))     # True
print(Decimal(0.1))          # 0.1000000000000000055511151231257827... - the float's real value
print(Decimal("0.1"))        # 0.1 - exactly what you wrote

Always construct a Decimal from a string. Passing a float hands it a value that was already inexact.

price = Decimal("19.99")
quantity = 3
tax_rate = Decimal("0.18")

subtotal = price * quantity
tax = (subtotal * tax_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
total = subtotal + tax

print(f"{subtotal}  {tax}  {total}")      # 59.97  10.79  70.76

getcontext().prec = 28                    # significant digits, default 28
print(Decimal(1) / Decimal(7))
UseFor
floatMeasurements, science, graphics, anything approximate
DecimalMoney, tax, invoices, anything an accountant will check
int of the smallest unitMoney, when you would rather avoid Decimal entirely

fractions

from fractions import Fraction

print(Fraction(1, 3))                       # 1/3
print(Fraction(1, 3) + Fraction(1, 6))      # 1/2 - exact
print(0.1 + 0.2 == 0.3)                     # False
print(Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10))     # True

print(Fraction("0.25"))                     # 1/4
print(Fraction(6, 8))                       # 3/4 - reduced automatically
print(float(Fraction(1, 3)))                # 0.3333333333333333
print(Fraction(1, 3).numerator, Fraction(1, 3).denominator)

Fractions are exact for any rational number, including thirds, which decimals cannot represent. Use them for exact ratios, probability and anything where 1/3 + 1/3 + 1/3 must equal exactly 1.

A worked comparison

from decimal import Decimal
from fractions import Fraction

third_float = 1 / 3
third_decimal = Decimal(1) / Decimal(3)
third_fraction = Fraction(1, 3)

print(third_float * 3)               # 1.0   (by luck of rounding)
print(third_decimal * 3)             # 0.9999999999999999999999999999
print(third_fraction * 3)            # 1     exactly

total = sum([Decimal("0.01")] * 100)
print(total, total == Decimal("1.00"))       # 1.00 True

Common mistakes

  • Constructing Decimal from a float instead of a string.
  • Confusing randint (inclusive) with randrange (exclusive).
  • Using random for tokens or passwords.
  • Expecting random.shuffle to return the shuffled list; it returns None.
  • Calling statistics.mode on data with a tie, and meeting StatisticsError.
  • Using math.pow when ** would have kept the result an integer.
  • Using float for money.

Best practices

  • Use math.isclose for every float comparison.
  • Use Decimal for currency, built from strings, and quantize before displaying.
  • Seed random in tests so failures reproduce.
  • Use secrets whenever unpredictability matters.
  • Use math.fsum when summing many floats.

Practice

  1. Compute compound interest with float and with Decimal over 120 months and compare the totals.
  2. Simulate 10 000 dice rolls and report the distribution, seeded so it reproduces.
  3. Write a function returning the mean, median and standard deviation of a list, handling the empty case.
  4. Explain why Decimal(0.1) and Decimal("0.1") differ, showing the output of each.
  5. Add one tenth ten times with float, Decimal and Fraction, and compare each to 1.

Conclusion

math for exact integer maths and float helpers, random for simulations, secrets for anything security related, statistics for summaries, and Decimal or Fraction when the answer has to be exact. Money is never a float.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.