Python Numbers: math, statistics, Decimal, Fraction, random
Python's math, statistics, Decimal, Fraction and random modules: exact integer roots, sample vs population stdev, money rounding and seeded randomness.
- Course: Python study plan
- Module: The standard library in depth
- Kind: Lesson
- Reading time: 14 min
- Runtime: CPython 3.11
When should you use Decimal instead of float in Python?
Use decimal.Decimal for money, invoices and anywhere a decimal rounding rule is required. A float stores binary fractions, so 0.1 + 0.2 is not exactly 0.3, while Decimal("0.1") + Decimal("0.2") is exactly Decimal("0.3"). Build a Decimal from a string, never a float, and round with quantize and a stated rounding mode such as ROUND_HALF_UP.
Lesson
The numeric modules divide the work cleanly: math for functions on ints and floats, statistics for descriptive statistics done correctly, fractions and decimal for the two kinds of exact arithmetic, random for reproducible pseudo-randomness and secrets for the unpredictable kind. This lesson goes through each with the functions that matter, the precision rules that decide which to use, and the two facts about random that judged and tested programs depend on: seeding makes it deterministic, and the module-level functions share one global generator.
math
import math
math.sqrt(2), math.isqrt(17) # 1.4142135623730951, 4 — float root, exact integer root
math.gcd(12, 18), math.lcm(4, 6) # 6, 12 (gcd takes any number of arguments since 3.9)
math.comb(5, 2), math.perm(5, 2) # 10, 20
math.factorial(20) # 2432902008176640000 — exact int
math.floor(-2.5), math.ceil(-2.5) # -3, -2 — ints
math.trunc(-2.5) # -2 — toward zero, like int()
math.log(100, 10), math.log2(1024), math.log10(1000), math.exp(1)
math.prod([2, 3, 4]) # 24 — the multiplicative sum
math.fsum([0.1] * 10) # 1.0 — accurate float summation
math.hypot(3, 4), math.dist((0, 0), (3, 4)) # 5.0, 5.0
math.isclose(0.1 + 0.2, 0.3) # True
math.pi, math.e, math.tau, math.inf, math.nan
math.radians(180), math.degrees(math.pi), math.sin, math.cos, math.atan2(y, x)
Integer functions (isqrt, gcd, lcm, comb, perm, factorial) return exact ints of any size; the float functions return floats and raise ValueError on domain errors (math.sqrt(-1)) and OverflowError on results too large. math.pow is always float — use ** for integer powers. math.isqrt(n) ** 2 == n is the perfect-square test; math.comb replaces the factorial formula and never overflows.
statistics
import statistics as st
st.mean([1, 2, 3, 4]) # 2.5
st.fmean([1, 2, 3, 4]) # 2.5 as a float, faster
st.median([3, 1, 2]) # 2
st.median([1, 2, 3, 4]) # 2.5 — the mean of the middle two
st.mode("aabbbc") # 'b'
st.multimode([1, 1, 2, 2]) # [1, 2]
st.stdev([2, 4, 4, 4, 5, 5, 7, 9]) # sample standard deviation (n − 1)
st.pstdev(...) # population standard deviation (n)
st.variance, st.pvariance
st.quantiles([1, 2, 3, 4, 5], n=4) # quartile cut points
mean of ints may return a Fraction-exact result converted to float; median of an even count averages the two middle values; stdev is the sample form — the one with n − 1 — and pstdev the population form; interviews ask which is which. All raise StatisticsError on empty input. These functions are correct on the edge cases a hand-written loop misses (mixed int/Fraction/Decimal input, catastrophic cancellation in variance).
fractions
from fractions import Fraction
Fraction(1, 3) + Fraction(1, 6) # Fraction(1, 2)
Fraction("0.1") + Fraction("0.2") # Fraction(3, 10) — exact
Fraction(0.1) # Fraction(3602879701896397, 36028797018963968) — the float's true value
Fraction(3, 4).numerator, Fraction(3, 4).denominator
float(Fraction(1, 3)), round(Fraction(7, 3)) # 0.333…, 2
Fraction(5, 10) # Fraction(1, 2) — always reduced
Fraction(1, 3).limit_denominator(100) # the closest fraction with denominator ≤ 100
Exact rational arithmetic: sums of fractions are fractions, comparisons are exact, and probability and ratio computations come out as 3/8 rather than 0.375000001. Construct from strings or int pairs, not floats. It is slow relative to floats and is used where exactness matters more than speed.
decimal
from decimal import Decimal, getcontext, ROUND_HALF_UP, ROUND_HALF_EVEN
getcontext().prec = 28 # significant digits (default 28)
Decimal("0.1") + Decimal("0.2") # Decimal('0.3')
Decimal("1") / Decimal("3") # Decimal('0.3333333333333333333333333333')
Decimal("2.675").quantize(Decimal("0.01")) # Decimal('2.68') — ROUND_HALF_EVEN by default: 2.68 (even)
Decimal("2.665").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # Decimal('2.67')
Decimal("1.10") + Decimal("2.20") # Decimal('3.30') — trailing zeros are significant
Decimal("100").sqrt(), Decimal("2").ln()
Decimal floating point with a context (precision and rounding mode) — the arithmetic finance and invoicing require. quantize rounds to a pattern (Decimal("0.01") for cents) with a stated rounding rule; the default is banker's rounding, and ROUND_HALF_UP is what accounting usually wants. localcontext() sets precision for a block. Construct from strings; Decimal(0.1) carries the float's error.
random
import random
rng = random.Random(42) # a private generator with a fixed seed — reproducible
rng.random() # a float in [0, 1)
rng.randint(1, 6) # inclusive both ends
rng.randrange(0, 10, 2) # like range
rng.choice(["a", "b", "c"])
rng.choices(items, weights=[5, 1], k=3) # with replacement, weighted
rng.sample(items, k=3) # without replacement
rng.shuffle(xs) # in place
rng.uniform(1.5, 2.5), rng.gauss(0, 1)
random.seed(42); random.randint(1, 6) # the module-level functions share ONE global generator
Two rules. Seed it and it is deterministic: the same seed gives the same sequence on every run and every platform for a given algorithm version, which is what tests and judged exercises need. Prefer a Random(seed) instance to the module functions: it cannot be disturbed by another part of the program calling random.random(). random is a Mersenne Twister — statistically fine, cryptographically useless: for tokens, passwords and anything an attacker may guess, secrets.token_hex(16), secrets.choice, secrets.randbelow.
Choosing
| Need | Use |
|---|---|
| Integer arithmetic, combinatorics, exact roots | int and math's integer functions |
| Scientific/geometric computation | float and math |
| Money, invoices, rounding rules | Decimal from strings with quantize, or integer cents |
| Ratios and probabilities that must be exact | Fraction |
| Descriptive statistics | statistics |
| Reproducible randomness | random.Random(seed) |
| Security-relevant randomness | secrets |
Pitfalls
math.sqrtfor integer roots (isqrt);math.powfor integer powers (**).stdevwherepstdevwas meant, or the reverse.Decimal(0.1)andFraction(0.1).randomfor anything security-related.- Module-level
random.seedin a library, resetting the caller's generator. - Expecting
round(2.675, 2)to give2.68— useDecimalandquantize.
Key takeaways
math: exact integer functions (isqrt,gcd,lcm,comb,factorial), float functions that raise on domain errors,fsum,isclose,prod.statistics:mean/fmean,median,mode, samplestdevversus populationpstdev,quantiles; empty input raises.Fractionfor exact ratios,Decimalwithquantizeand a rounding mode for money — both built from strings.random.Random(seed)is reproducible and isolated; the module functions share one global state;secretsfor anything guessable.- Pick the numeric type by the guarantee you need, not by habit.
Common questions
What is the difference between stdev and pstdev in Python?
statistics.stdev is the sample standard deviation, whose variance divides by n − 1; statistics.pstdev is the population standard deviation, whose variance divides by n. Use stdev when the data is a sample of a larger population and pstdev when it is the whole population. Both raise StatisticsError on empty input.
Why does round(2.675, 2) give 2.67 in Python?
The float literal 2.675 is stored as a binary value slightly below 2.675, so rounding to two places gives 2.67. For decimal rounding, use Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), which gives Decimal("2.68"); the default banker's rounding also gives 2.68 here, because 8 is even.
How do I make random numbers reproducible in Python?
Seed a private generator: rng = random.Random(42) produces the same sequence on every run for a given algorithm version, which tests and judged programs need. Prefer the instance to random.seed, because the module-level functions share one global generator that any other code can disturb.
What is the difference between random and secrets in Python?
random is a Mersenne Twister: statistically fine for simulations and games but predictable, so useless for security. secrets uses a cryptographically strong source and is the module for tokens, passwords and anything an attacker might guess, with secrets.token_hex(16), secrets.choice and secrets.randbelow.
Exercises
Number report
Read a line of positive integers and print: the gcd and lcm of all of them (math.gcd/math.lcm accept many arguments), the integer square root of the largest, which values are perfect squares (isqrt(n) ** 2 == n), math.comb(n, 2) for n = the count of values, and the sample and population standard deviations from statistics with three decimals (n/a for the sample deviation when there is only one value).
Input: one line of positive integers (at least one). Output: gcd <g> lcm <l>, isqrt <r>, squares <values or none>, pairs <c>, stdev <s> pstdev <p>.
12 18 36
prints
gcd 6 lcm 36
isqrt 6
squares 36
pairs 3
stdev 12.490 pstdev 10.198Seeded dice
Randomness must be reproducible in a judged program. Read a seed and n; create random.Random(seed); print n rolls of randint(1, 6), then a shuffled copy of [1, 2, 3, 4, 5] (shuffle in place on a copy), then sample(range(100), 3), then a choice from "abc". Every value comes from the same generator in that order.
Input: seed n. Output: rolls …, shuffled …, sample …, choice <c>.
42 5
prints
rolls 6 1 1 6 3
shuffled 4 3 1 5 2
sample 86 94 69
choice aIn this module: The standard library in depth
- math, statistics, fractions, decimal and random (this lesson)
- Dates and times — datetime, timedelta and zoneinfo
- collections in depth — deque, Counter, OrderedDict, ChainMap and the User classes
- Text-processing tools — textwrap, difflib, string.Template and re in depth
- Enums — Enum, IntEnum, StrEnum, Flag and auto
- Checkpoint — The standard library in depth
← Checkpoint — Modules, packages and imports · Dates and times — datetime, timedelta and zoneinfo →