The number sets

Numbers come in families. Each family was invented because the previous one could not answer some question: you cannot subtract 5 from 3 with only counting numbers, and you cannot divide 1 by 3 with only whole numbers. Mathematicians give each family a name and a letter in "blackboard bold", a font with doubled strokes.

Natural numbers

The natural numbers are the counting numbers, including zero:

\mathbb{N} = \{0, 1, 2, 3, \dots\}

The curly brackets mean "the set of", and the dots mean "and so on, forever". Natural numbers count things: tickets in a queue, disks in a server, bits in a byte. Adding or multiplying two natural numbers always gives a natural number again, but subtracting does not: 3 − 5 has no answer in \mathbb{N}.

Integers

Add the negative whole numbers and you get the integers, written \mathbb{Z} (from the German Zahlen, "numbers"):

\mathbb{Z} = \{\dots, -3, -2, -1, 0, 1, 2, 3, \dots\}

Now subtraction always works: 3 − 5 = −2. Integers describe differences and directions: a temperature below zero, an account balance in the red, a clock that is 2 hours behind UTC. In Python they are the int type, which has no upper limit. Most other languages store integers in a fixed number of bits, which is why they can overflow.

Rational numbers

Dividing integers does not always give an integer: 1 / 3 is not a whole number. A rational number is any number that can be written as a fraction of two integers, with a denominator that is not zero:

\mathbb{Q} = \left\{ \frac{p}{q} \;\middle|\; p, q \in \mathbb{Z},\ q \ne 0 \right\}

Read this as "the set of all p over q, where p and q are integers and q is not zero". The vertical bar means "such that", and \in means "is an element of". The letter \mathbb{Q} comes from "quotient", the result of a division.

Every integer is rational too: 7 = 7/1. Written as a decimal, a rational number either stops (1/8 = 0.125) or repeats the same block of digits forever (1/3 = 0.333…, 2/7 = 0.285714285714…). That is how you can recognize one.

Irrational numbers

Some numbers cannot be written as a fraction at all. An irrational number has a decimal expansion that never stops and never repeats. The best-known examples:

  • \sqrt{2} = 1.41421356\dots, the length of the diagonal of a square with sides of 1.
  • \pi = 3.14159265\dots, the circumference of a circle divided by its diameter.
  • e = 2.71828182\dots, the base of the natural logarithm, which appears wherever things grow continuously.

A computer can never store an irrational number exactly; it always stores an approximation with a fixed number of digits. For everyday IT work that is fine, as long as you know it is happening.

Real numbers

The rational and irrational numbers together form the real numbers, \mathbb{R}: every point on the number line. Measurements such as a response time of 0.0025 seconds, a power of 3.5 kW or an angle of 45 degrees are real numbers.

A number line from minus 3 to 4 with the integers marked as ticks. Four real numbers are marked as dots: minus 1.5, one half, the square root of 2 (about 1.41) and pi (about 3.14). −3 −2 −1 0 1 2 3 4 −1.5 1/2 √2 π
The real number line. Integers sit on the ticks; rational numbers such as −1.5 and 1/2 and irrational numbers such as √2 and π fill the gaps between them.

Each set contains the previous one. Mathematicians write this with the subset symbol \subset:

\mathbb{N} \subset \mathbb{Z} \subset \mathbb{Q} \subset \mathbb{R}
Four nested rectangles. The innermost, the natural numbers, holds 0, 1, 2, 42 and 1024. Around it the integers add minus 1 and minus 273. Around that the rational numbers add one half, minus 0.75 and 0.333 repeating. The outermost, the real numbers, adds the square root of 2, pi and e. ℝ real numbers ℚ rational numbers ℤ integers ℕ natural numbers 0 1 2 42 1024 −1 −273 1/2 −0.75 0.333… √2 π e
Each set of numbers contains the one inside it: every natural number is an integer, every integer is rational, every rational number is real.

Beyond the real numbers

Even the real numbers have a gap: no real number squared gives −1. The complex numbers, \mathbb{C}, fill it with a new number i for which i^2 = -1. They matter in signal processing and physics, but you will rarely need them in IT. You may meet them by accident in Python, which has a built-in complex type: (-8) ** (1 / 3) returns a complex number instead of −2 (see the roots section).

Number sets in Python

Python has a type for each practical need: int for integers, Fraction for exact rational numbers, and float for real numbers, stored as close approximations:

from fractions import Fraction

print(type(7), type(7.0), type(Fraction(7, 2)).__name__)
print(Fraction(1, 3) + Fraction(1, 6))
print(1 / 3 + 1 / 6)
print(Fraction(0.5), Fraction("0.1"))
<class 'int'> <class 'float'> Fraction
1/2
0.5
1/2 1/10

The fraction 1/3 + 1/6 is exactly 1/2. The float version also prints 0.5 here, but only because the small rounding errors happen to cancel out; the floating-point page shows cases where they do not.

Order of operations

What is 2 + 3 × 4? If you work from left to right you get 20; the accepted answer is 14. The order of operations is the agreement that decides which part of a calculation is done first, so that everyone gets the same answer:

  1. Brackets: whatever is inside them first.
  2. Powers (and roots), such as 4^2.
  3. Multiplication and division, with equal priority, from left to right.
  4. Addition and subtraction, with equal priority, from left to right.

Worked example: 2 + 3 \cdot 4^2.

  1. No brackets, so start with the power: 4^2 = 16.
  2. Then the multiplication: 3 \cdot 16 = 48.
  3. Then the addition: 2 + 48 = 50.

With brackets, (2 + 3) \cdot 4^2 is 5 \cdot 16 = 80. Two more rules catch many people out:

  • A minus sign in front binds weaker than a power. -2^2 means -(2^2) = -4.
  • Stacked powers are worked out from the top down. 2^{3^2} means 2^9 = 512, not 8^2 = 64.

Python follows the same rules:

print(2 + 3 * 4 ** 2)
print((2 + 3) * 4 ** 2)
print(-2 ** 2)
print((-2) ** 2)
print(2 ** 3 ** 2)
print(12 / 4 * 3)
50
80
-4
4
512
9.0

The last line shows "left to right" for operations of equal priority: 12 / 4 is 3, then 3 × 3 is 9. Python's full list of priorities, including comparison and logical operators, is on the operator precedence section of the Python pages, with the right-to-left rule for ** under associativity.

Fractions

A fraction \frac{a}{b} means "a divided by b". The top number is the numerator, the bottom one the denominator. Fractions turn up in IT as ratios (a 16:9 screen), shares (3 of 4 servers healthy) and exact amounts that decimals cannot write (one third of a budget).

Simplifying

Dividing the top and bottom by the same number does not change a fraction's value. To simplify 18/24:

  1. Find a number that divides both: 6 divides 18 and 24.
  2. Divide both: 18 / 6 = 3 and 24 / 6 = 4.
  3. So \frac{18}{24} = \frac{3}{4}.

The largest number that divides both is their greatest common divisor; dividing by it gives the simplest form in one step.

Adding and subtracting

You can only add fractions with the same denominator: one third plus one sixth are pieces of different sizes. Worked example, \frac{1}{3} + \frac{1}{6}:

  1. Find a common denominator: 6 is a multiple of both 3 and 6.
  2. Rewrite 1/3 with denominator 6 by multiplying top and bottom by 2: \frac{1}{3} = \frac{2}{6}.
  3. Add the numerators and keep the denominator: \frac{2}{6} + \frac{1}{6} = \frac{3}{6}.
  4. Simplify: \frac{3}{6} = \frac{1}{2}.

Multiplying and dividing

Multiplying is easier: multiply the tops and multiply the bottoms. Dividing by a fraction is multiplying by its upside-down version:

\frac{3}{4} \cdot \frac{2}{5} = \frac{3 \cdot 2}{4 \cdot 5} = \frac{6}{20} = \frac{3}{10} \qquad \frac{3}{4} \div \frac{2}{5} = \frac{3}{4} \cdot \frac{5}{2} = \frac{15}{8}
from fractions import Fraction

print(Fraction(3, 4) * Fraction(2, 5))
print(Fraction(3, 4) / Fraction(2, 5))
print(Fraction(18, 24))
print(1 / 8, 1 / 3, 2 / 7)
3/10
15/8
3/4
0.125 0.3333333333333333 0.2857142857142857

Fraction always simplifies its result. The last line shows the decimal forms: 1/8 stops, while 1/3 and 2/7 would repeat forever, so the float is cut off after about 16 significant digits.

Percentages

"Per cent" means "per hundred": 21% is \frac{21}{100} = 0.21. To calculate with a percentage, first turn it into a decimal by dividing by 100.

A percentage of an amount

The standard rate of Dutch VAT (value added tax) is 21%. On an amount of €80.00 before tax:

  1. 21% = 0.21.
  2. The tax: 0.21 × 80.00 = €16.80.
  3. The total: 80.00 + 16.80 = €96.80, which is the same as 1.21 × 80.00 in one step.
  4. Back from the total to the amount before tax: divide by 1.21, not subtract 21%. 96.80 / 1.21 = €80.00, while 96.80 minus 21% of 96.80 would give €76.47.

The UK standard rate is 20%. The United States has no national VAT; the sales tax set by states and local governments is usually added at the checkout, and the same "divide, don't subtract" rule applies when you take tax out of a receipt total.

Percentage change

Kestrel Energy raises its tariff from €0.28 to €0.31 per kWh. The relative change is the difference divided by the old value:

\text{change} = \frac{\text{new} - \text{old}}{\text{old}} = \frac{0.31 - 0.28}{0.28} = \frac{0.03}{0.28} \approx 0.107 = 10.7\%

A rise of 10% followed by a fall of 10% does not bring you back to the start: 100 becomes 110, and 10% of 110 is 11, so you end at 99. The second percentage is taken of a different, larger amount.

Percentage points

When the thing that changes is itself a percentage, there are two ways to describe the change, and they are easy to confuse. Kestrel's portal availability goes from 99.5% to 99.9%:

  • The difference is 0.4 percentage points: 99.9 − 99.5 = 0.4. Percentage points are the plain difference between two percentages.
  • The relative change is 0.4 / 99.5 ≈ 0.4% of the old value, which sounds much smaller and says something else.

For availability, the downtime is the better measure. 99.5% leaves 0.5% downtime and 99.9% leaves 0.1%: five times less, even though the percentages look almost the same.

old, new = 0.28, 0.31          # euros per kWh
change = (new - old) / old
print(f"Tariff change: {change:.1%}")
print(f"Difference: {(new - old) * 100:.0f} cents per kWh")
availability_before, availability_after = 0.995, 0.999
print(f"{(availability_after - availability_before) * 100:.1f} percentage points")
Tariff change: 10.7%
Difference: 3 cents per kWh
0.4 percentage points

The format :.1% multiplies by 100, rounds to one decimal and adds the percent sign. The same idea turns availability targets into minutes of allowed downtime in a 30-day month, a common question when reading a service level agreement:

# Downtime allowed by an availability target, per 30-day month
minutes = 30 * 24 * 60
for target in (0.99, 0.999, 0.9999):
    print(f"{target:.2%}: {(1 - target) * minutes:.1f} minutes")
99.00%: 432.0 minutes
99.90%: 43.2 minutes
99.99%: 4.3 minutes

Rounding

Rounding replaces a number by a nearby, shorter one. To round to two decimals, look at the third decimal: below 5, keep the second decimal; 5 or more, raise it by one. So 3.14159 becomes 3.14 and 2.678 becomes 2.68.

The trouble with halves

The rule "5 or more goes up" is what most people learn at school (round half up). It has a small bias: across many numbers, it rounds up slightly more often than down. Python's round() therefore uses another rule, which also is the default in the floating-point standard: a value exactly halfway goes to the nearest even digit. This is called round half to even, or banker's rounding, and it is explained in detail on the floating-point page.

from decimal import Decimal, ROUND_HALF_UP

print(round(0.5), round(1.5), round(2.5), round(3.5))
print(round(2.675, 2))
print(Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
print(Decimal("2.665").quantize(Decimal("0.01")))
0 2 2 4
2.67
2.68
2.66
  1. Line 1: 0.5, 1.5, 2.5 and 3.5 are exactly halfway, so they go to the even neighbor: 0, 2, 2 and 4.
  2. Line 2: 2.675 should round to 2.68 by either rule, yet Python gives 2.67. The float 2.675 is stored as a binary approximation that is slightly less than 2.675, so it is not a half at all (see floating point).
  3. Line 3: Decimal stores decimal digits exactly, and ROUND_HALF_UP gives the school answer, 2.68.
  4. Line 4: without a rounding argument, Decimal also rounds halves to even: 2.665 becomes 2.66.

For invoices, use Decimal with the rounding rule your finance department prescribes, and round once, at the end. Other languages have the same tool, such as C#'s decimal.

Truncating and rounding down

Cutting off the decimals (truncating) is not the same as rounding down. For positive numbers they agree; for negative numbers they do not, because "down" means toward minus infinity:

import math

print(int(2.9), int(-2.9))
print(math.floor(-2.9), math.ceil(-2.1))
2 -2
-3 -2

int() truncates toward zero, so −2.9 becomes −2; math.floor rounds down, so −2.9 becomes −3. The floor and ceiling functions are introduced on the first page of this topic.

Scientific notation

Very large and very small numbers are hard to read: is 0.0000025 two and a half millionths or two and a half ten-millionths? Scientific notation writes every number as a number between 1 and 10 (the coefficient) times a power of 10:

149{,}600{,}000{,}000 = 1.496 \times 10^{11} \qquad 0.0025 = 2.5 \times 10^{-3}

Worked example: write 0.0025 seconds (a 2.5 ms latency) this way.

  1. Move the decimal point until exactly one non-zero digit is in front of it: 0.0025 becomes 2.5.
  2. Count the moves: three places to the right.
  3. Moving right makes the number bigger, so compensate with a negative power: 2.5 \times 10^{-3}. (10^{-3} is 1/1000; see negative exponents.)

For a large number, the point moves left and the power is positive: the 11 in 1.496 \times 10^{11} says the point moved 11 places. Programming languages write the "times 10 to the power" part as e: 1.496e+11 and 2.5e-03.

distance_m = 149_600_000_000      # Earth to Sun, in meters
latency_s = 0.0025                # 2.5 milliseconds
print(f"{distance_m:.3e}")
print(f"{latency_s:.1e}")
print(6.022e23 * 2)
print(1e-3 == 0.001)
1.496e+11
2.5e-03
1.2044e+24
True

The underscores in 149_600_000_000 are only for readability; Python ignores them. The format :.3e shows three digits after the point in scientific notation.

The number of meaningful digits in a value is its number of significant figures: 1.496 × 1011 has four. A Python float holds about 15 to 17 significant decimal digits, which is why 1/3 printed as 0.3333333333333333 above. Metric prefixes are the everyday version of the same idea: 2.5 × 10−3 seconds is 2.5 milliseconds. The units and prefixes page covers them, including the binary prefixes used for memory.