Codetail

Article 3 of 15

Numbers & Math

Integers, floats, precision, real-world math.

18 min read

Integers

Python integers are exact and have no upper bound. Most languages give you a fixed number of bits (32 or 64) and overflow silently. Python integers grow as large as your memory allows.

Python
1# No overflow in Python
2x = 2 ** 64
3print(x) # 18446744073709551616: bigger than any 64-bit int
4
5# Factorial of 100: try this in C
6import math
7print(math.factorial(100))
8# 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

Integer literals

Python lets you write integer literals in four bases. All four produce the same kind of object. The base only affects how you write the number, not how Python stores it.

Python
1decimal = 255 # base 10 (default)
2hexadecimal = 0xFF # base 16: common for colors, bitmasks
3octal = 0o377 # base 8: file permissions (chmod 644)
4binary = 0b11111111 # base 2: bitwise operations
5
6print(decimal == hexadecimal == octal == binary) # True: all 255
7print(hex(255)) # '0xff'
8print(bin(255)) # '0b11111111'
9print(oct(255)) # '0o377'

Underscore separators

Underscores in numeric literals are ignored by Python. Use them to make large numbers readable, the same way you'd use commas in prose.

Python
1population = 8_100_000_000 # 8.1 billion: easy to read
2max_int_32 = 2_147_483_647 # clear grouping
3hex_color = 0xFF_A5_00 # orange in hex: grouped by channel
4
5print(population) # 8100000000: underscores stripped

Bitwise operators

Python supports bitwise operations on integers. You'll use these for flags, permissions, and low-level data manipulation.

Python
1a = 0b1010 # 10
2b = 0b1100 # 12
3
4print(bin(a & b)) # AND: 0b1000 (8), bits set in both
5print(bin(a | b)) # OR: 0b1110 (14), bits set in either
6print(bin(a ^ b)) # XOR: 0b0110 (6), bits set in exactly one
7print(bin(~a)) # NOT: -0b1011, flip all bits
8print(bin(a << 1)) # left shift: 0b10100 (20), multiply by 2
9print(bin(a >> 1)) # right shift: 0b0101 (5), divide by 2

Floats and why they lie

Floats are Python's decimal numbers. They're fast and cover an enormous range, but they have a fundamental limitation that trips up almost every programmer.

Python
1print(0.1 + 0.2) # 0.30000000000000004
2print(0.1 + 0.2 == 0.3) # False

This isn't a Python bug. It's how floating-point math works in every language: C, Java, JavaScript, Go. Python is just honest about it.

Why it happens

Computers store numbers in binary (base 2). Just like 1/3 has no exact decimal representation (0.333...), 1/10 has no exact binary representation. It's a repeating fraction in binary: 0.0001100110011...

Python stores floats using 64 bits (the IEEE 754 double precision standard). That gives you about 15-17 significant digits. The closest 64-bit float to 0.1 is not exactly 0.1, it's:

Python
1# The actual value Python stores for 0.1
2from decimal import Decimal
3print(Decimal(0.1))

When you add two slightly-off approximations of 0.1, you get an approximation of 0.2 that's slightly different from the closest approximation of 0.3. The errors compound. This is expected behavior, not a bug.

Comparing floats correctly

Never use == to compare floats. Use math.isclose(), which checks whether two floats are close enough to be considered equal given the inherent imprecision.

Python
1import math
2
3a = 0.1 + 0.2
4b = 0.3
5
6print(a == b) # False, never use == for floats
7print(math.isclose(a, b)) # True
8print(math.isclose(a, b, rel_tol=1e-9)) # True, default tolerance
9print(abs(a - b) < 1e-9) # True, manual version of the same check

Special float values

Python floats can represent infinity and "not a number." These arise from certain operations rather than being written directly in most code.

Python
1import math
2
3inf = math.inf # positive infinity
4print(inf) # inf
5print(-inf) # -inf
6print(inf + 1) # inf, infinity absorbs addition
7print(inf > 1e308) # True, larger than any finite float
8
9nan = math.nan # "not a number"
10print(nan) # nan
11print(nan == nan) # False, nan is never equal to anything, even itself
12print(math.isnan(nan)) # True, correct way to check
13
14# These operations produce inf or nan
15print(1.0 / 0.0) # ZeroDivisionError for floats
16print(float('inf')) # construct from string
17print(float('nan'))

Float precision in practice

Python
1# Rounding for display, doesn't change the stored value
2x = 3.141592653589793
3print(round(x, 2)) # 3.14
4print(f"{x:.4f}") # "3.1416": formatted string
5
6# repr() shows enough digits to reconstruct the float exactly
7print(repr(0.1)) # 0.1: Python is smart about display
8print(repr(0.10000000000000001)) # 0.1: same float, same repr
9
10# sys.float_info shows the limits of your platform's floats
11import sys
12print(sys.float_info.max) # ~1.8e+308
13print(sys.float_info.epsilon) # ~2.2e-16, smallest difference from 1.0

Arithmetic operators

Python has seven arithmetic operators. Most behave as expected, but three have gotchas worth knowing upfront.

OperatorNameExampleResult
+Addition3 + 25
-Subtraction3 - 21
*Multiplication3 * 26
/Division7 / 23.5 (always float)
//Floor division7 // 23 (integer result)
%Modulo7 % 21 (remainder)
**Exponentiation2 ** 101024

Division always returns a float

In Python 3, / always returns a float, even when dividing two integers evenly. Use // when you need an integer result.

Python
1print(10 / 2) # 5.0: float, even though it divides evenly
2print(10 // 2) # 5: integer (floor division)
3print(7 / 2) # 3.5
4print(7 // 2) # 3: rounds toward negative infinity, not zero

Floor division rounds toward negative infinity

// always rounds down, meaning toward negative infinity, not toward zero. This matters with negative numbers.

Python
1print(7 // 2) # 3: expected
2print(-7 // 2) # -4: rounds toward -inf, not -3
3print(7 // -2) # -4: same rule
4
5# Compare with int() which truncates toward zero
6print(int(-7 / 2)) # -3: truncates toward zero
7print(-7 // 2) # -4: floors toward -infinity

Modulo: remainder after division

The % operator gives the remainder after floor division. Classic uses: checking for even/odd, wrapping around a range, extracting digits.

Python
1# Even/odd
2print(10 % 2) # 0: even
3print(7 % 2) # 1: odd
4
5# Wrapping: keep index in range 0..n-1
6index = 7
7n = 5
8print(index % n) # 2: wraps around
9
10# Extract last two digits of a year
11year = 2025
12print(year % 100) # 25
13
14# Check divisibility
15for n in range(1, 16):
16 if n % 3 == 0 and n % 5 == 0:
17 print("FizzBuzz")
18 elif n % 3 == 0:
19 print("Fizz")
20 elif n % 5 == 0:
21 print("Buzz")

Exponentiation

Python
1print(2 ** 10) # 1024
2print(2 ** 0.5) # 1.4142...: square root via fractional exponent
3print((-1) ** 0.5) # (6.123233995736766e-17+1j), complex number!
4
5# Faster for large exponents: pow() with three arguments does modular exponentiation
6print(pow(2, 100, 1000)) # 2^100 mod 1000 = 376, fast even for huge exponents

Operator precedence

Python follows standard math precedence. When in doubt, use parentheses, they make intent explicit and are never wrong.

Python
1# PEMDAS/BODMAS applies
2print(2 + 3 * 4) # 14, not 20: * before +
3print((2 + 3) * 4) # 20: parens override
4
5# ** binds tighter than unary minus, this surprises people
6print(-2 ** 2) # -4, not 4: reads as -(2**2)
7print((-2) ** 2) # 4: use parens when you mean negative base

Math functions

Python gives you math tools in two places: built-in functions available everywhere, and the math module for everything else.

Built-in functions

Python
1# abs(): absolute value
2print(abs(-7)) # 7
3print(abs(3.14)) # 3.14
4
5# round(): round to n decimal places
6print(round(3.14159, 2)) # 3.14
7print(round(3.14159, 0)) # 3.0: still a float
8print(round(3.14159)) # 3: integer when no ndigits
9
10# min() and max()
11print(min(3, 1, 4, 1, 5)) # 1
12print(max(3, 1, 4, 1, 5)) # 5
13print(min([3, 1, 4, 1, 5])) # 1: also accepts a list
14
15# sum()
16print(sum([1, 2, 3, 4, 5])) # 15
17print(sum(range(1, 101))) # 5050: Gauss's formula check
18
19# divmod(): quotient and remainder in one call
20q, r = divmod(17, 5)
21print(q, r) # 3 2: same as (17 // 5, 17 % 5)
22
23# pow(): same as ** but accepts an optional modulus
24print(pow(2, 10)) # 1024
25print(pow(2, 10, 1000)) # 24: 1024 mod 1000

Gotcha with round():Python uses banker's rounding (round half to even). So round(0.5) is 0 and round(1.5) is 2. This reduces cumulative rounding error in statistical calculations, but it surprises people expecting standard rounding.

The math module

Python
1import math
2
3# Constants
4print(math.pi) # 3.141592653589793
5print(math.e) # 2.718281828459045
6print(math.tau) # 6.283...: 2 * pi
7print(math.inf) # inf
8
9# Rounding
10print(math.floor(3.9)) # 3: always rounds down
11print(math.ceil(3.1)) # 4: always rounds up
12print(math.trunc(3.9)) # 3: truncates toward zero
13print(math.trunc(-3.9)) # -3: toward zero (unlike floor)
14
15# Roots and powers
16print(math.sqrt(16)) # 4.0
17print(math.isqrt(17)) # 4: integer square root (floor)
18
19# Logarithms
20print(math.log(math.e)) # 1.0: natural log
21print(math.log(100, 10)) # 2.0: log base 10
22print(math.log2(1024)) # 10.0: log base 2
23print(math.log10(1000)) # 3.0
Python
1import math
2
3# Trig (angles in radians)
4print(math.sin(math.pi / 2)) # 1.0
5print(math.cos(0)) # 1.0
6print(math.degrees(math.pi)) # 180.0
7print(math.radians(180)) # 3.14159...
8
9# Useful utilities
10print(math.factorial(10)) # 3628800
11print(math.gcd(48, 18)) # 6: greatest common divisor
12print(math.lcm(4, 6)) # 12: least common multiple (Python 3.9+)
13print(math.comb(10, 3)) # 120: "10 choose 3" combinations
14print(math.perm(10, 3)) # 720: permutations
15
16# Float comparison (the right way)
17print(math.isclose(0.1 + 0.2, 0.3)) # True
18print(math.isfinite(math.inf)) # False
19print(math.isinf(math.inf)) # True
20print(math.isnan(float('nan'))) # True

Decimal: when you need exact math

For money, taxes, and anything where a penny of error is unacceptable, use decimal.Decimal. It stores numbers as exact decimal values, no binary approximation.

Python
1from decimal import Decimal
2
3# floats lie about money
4price = 1.10
5tax_rate = 0.0825
6total = price * (1 + tax_rate)
7print(total) # 1.1907500000000002: wrong
8
9# Decimal gives exact results
10price = Decimal("1.10") # use strings, not floats
11tax_rate = Decimal("0.0825")
12total = price * (1 + tax_rate)
13print(total) # 1.19075: correct

Always construct Decimal from a string, not a float. Decimal(0.1) captures the imprecise float value. Decimal("0.1") gives you the exact decimal 0.1.

Controlling precision and rounding

Python
1from decimal import Decimal, ROUND_HALF_UP, getcontext
2
3# Set global precision (default is 28 significant digits)
4getcontext().prec = 6
5
6result = Decimal("1") / Decimal("3")
7print(result) # 0.333333: respects precision setting
8
9# Round to 2 decimal places for display
10price = Decimal("19.999")
11rounded = price.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
12print(rounded) # 20.00
13
14# All arithmetic operations return Decimal
15a = Decimal("0.10")
16b = Decimal("0.20")
17print(a + b) # 0.30: exact

fractions.Fraction: exact rational math

For exact fraction arithmetic (1/3 + 1/6 = 1/2), use fractions.Fraction. Less common than Decimal, but useful in geometry and statistics.

Python
1from fractions import Fraction
2
3a = Fraction(1, 3) # exactly 1/3
4b = Fraction(1, 6) # exactly 1/6
5
6print(a + b) # 1/2: exact
7print(a * b) # 1/18
8print(float(a + b)) # 0.5

Use float for science, graphics, and performance-critical code. Use Decimal for money and anything that will be displayed to users as a dollar amount. Use Fraction for exact rational arithmetic where the denominator matters.

Real-world patterns

The numeric operations you'll reach for constantly in production.

Clamp a value to a range

Keep a value between a minimum and maximum. Common in UI (slider values, opacity), game logic (health points), and input validation.

Python
1def clamp(value, low, high):
2 return max(low, min(high, value))
3
4print(clamp(5, 0, 10)) # 5: already in range
5print(clamp(-3, 0, 10)) # 0: below min, snapped to 0
6print(clamp(15, 0, 10)) # 10: above max, snapped to 10
7
8# Python 3.9+ has math.clamped equivalent in many contexts
9# but the above is idiomatic and clear

Percentage calculations

Python
1original = 120
2discounted = 90
3
4# What percentage is discounted of original?
5pct = (discounted / original) * 100
6print(f"{pct:.1f}%") # 75.0%
7
8# How much was discounted?
9saving_pct = ((original - discounted) / original) * 100
10print(f"Saved {saving_pct:.0f}%") # Saved 25%
11
12# Apply a percentage increase
13price = 100
14markup = 1.15 # 15% markup
15print(price * markup) # 115.0

Safe division

Division by zero raises an exception. Guard against it when the denominator comes from user input or data.

Python
1def safe_divide(a, b, fallback=0):
2 return a / b if b != 0 else fallback
3
4print(safe_divide(10, 2)) # 5.0
5print(safe_divide(10, 0)) # 0
6print(safe_divide(10, 0, fallback=None)) # None

Format numbers for display

Python
1# Thousands separator
2print(f"{1_234_567:,}") # 1,234,567
3print(f"{1_234_567.89:,.2f}") # 1,234,567.89
4
5# Fixed decimal places
6print(f"{3.14159:.2f}") # 3.14
7print(f"{0.0042:.4f}") # 0.0042
8print(f"{0.0042:.2e}") # 4.20e-03: scientific notation
9
10# Padding for alignment
11for n in [1, 10, 100, 1000]:
12 print(f"{n:>6,}") # right-aligned, 6 wide

Integer math tricks

Python
1# Check if a number is a power of two
2def is_power_of_two(n):
3 return n > 0 and (n & (n - 1)) == 0
4
5print(is_power_of_two(8)) # True
6print(is_power_of_two(10)) # False
7
8# Count digits without converting to string
9import math
10def count_digits(n):
11 return math.floor(math.log10(abs(n))) + 1 if n != 0 else 1
12
13print(count_digits(12345)) # 5
14print(count_digits(0)) # 1
15
16# Round up to the nearest multiple
17def round_up_to(n, multiple):
18 return math.ceil(n / multiple) * multiple
19
20print(round_up_to(23, 5)) # 25
21print(round_up_to(20, 5)) # 20