Skip to content

Instantly share code, notes, and snippets.

@joezeng
Last active July 8, 2026 15:02
Show Gist options
  • Select an option

  • Save joezeng/d5d562ff34b390f20c22405b6bc9e99e to your computer and use it in GitHub Desktop.

Select an option

Save joezeng/d5d562ff34b390f20c22405b6bc9e99e to your computer and use it in GitHub Desktop.
Falsehoods programmers believe about numbers

Falsehoods programmers believe about numbers

  1. Numbers always have exact expressions in data.

  2. Numbers always have exact expressions in data if I restrict the significant digits I work with and round them off hard enough.

  3. Multiplication is associative and commutative.

  4. Multiplication is invertible.

  5. Addition is associative and commutative.

  6. Addition is invertible.

  7. Multiplication is distributive over addition.

  8. Repeatedly adding a number n times will get me the same result as multiplying that number by n.

  9. Repeatedly adding a number n will get me the same side of the inequality as multiplying that number by n.

  10. All of the above statements are true if I use a decimal data format instead of IEEE 754 floating-point.

  11. All the above statements are true if I use an equality function that allows for a margin of error.

  12. All of the above statements are true if I use integers only.

  13. Multiplying two integers together will always give me the exact correct answer.

  14. Adding two integers together will always give me the exact correct answer.

  15. Numbers will always equal what I assign them.

  16. Numbers always have exactly one decimal representation.

  17. Numbers always have at least one decimal representation.

  18. Negative numbers always exist.

  19. Fractional numbers always exist.

  20. Integer division is always truncated.

  21. Subtracting 1 modulo n does the same thing as adding n-1.

  22. Integers are stored in memory in the order I read them (big-endian format).

  23. Integers are stored in little-endian format.

  24. Integers are stored in a format with fixed endianness.

  25. Rounded percentages always add up to 100%.

  26. Rounded percentages always add up to 100%, given enough precision.

  27. Numbers always have commas as thousands separators.

  28. The decimal point is always a period.

  29. Numbers are always separated by the thousands.

  30. Numbers are always separated by equal groups of digits.

  31. Numbers are always grouped the same way when written out in words versus when written out in numerals.

  32. Numbers are always grouped when there are too many digits to fit in a single group.

  33. When abbreviating numbers, the same prefix/suffix always referes to the same number.

  34. When writing numbers out in words, the same word always refers to the same number.

  35. The same number always has the same prefix or suffix.

  36. Numbers are always written in base ten.

  37. Numbers are always written in base 2, 8, 10, or 16.

  38. Place values are always written starting with 0, 1, 2, etc.

  39. Numbers are always written with one character per digit.

  40. Numbers are always written with Arabic numerals.

  41. Numbers are always written with numerals.

  42. Numbers always exist.

Explanations

Numbers always have exact expressions in data.

IEEE 754 is great for expressing mostly-accurate quantities, not so great when you want decimal precision. The following examples will all be done in Python, which uses IEEE 754 for its floating-point operations.

(I'm using Python 3.6.3 / IPython 6.2.1 if you're interested in reproducing my setup, but I think these examples should be the same in all versions of Python >= 3.0.)

In [1]: 0.1 + 0.2
Out[1]: 0.30000000000000004

There's actually a whole website dedicated to showcasing this "bug" in different languages: http://0.30000000000000004.com/

Numbers always have exact expressions in data if I restrict the significant digits I display and round them off hard enough.

It's not that simple. Numbers not having exact expressions leads to basically all the assumptions you'd make about arithmetic being incorrect in certain cases.

Multiplication is associative and commutative.

In [2]: a = 1/10; b = 1/11; c = 1/12

In [3]: a * b * c == a * c * b
Out[3]: False

Multiplication is invertible.

In [4]: 1.0 / 49.0 * 49.0 == 1.0
Out[4]: False

Addition is associative and commutative.

In [5]: a = 1/3; b = 1/4; c = 1/5

In [6]: a + b + c == a + c + b
Out[6]: False

Addition is invertible.

In [7]: 1/2 + 1/3 - 1/3 == 1/2
Out[7]: False

Multiplication is distributive over addition.

In [8]: a = 1/3; b = 1/4

In [9]: 3 * (a + b) == 3 * a + 3 * b
Out[9]: False

Repeatedly adding a number n times will get me the same number as multiplying that number by n.

In [10]: 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 0.1 * 10
Out[10]: False

Repeatedly adding a number n times will get me the same side of the inequality as multiplying that number by n.

In [11]: sum(0.1 for i in range(48))
Out[11]: 4.799999999999999

In [12]: 0.1 * 48
Out[12]: 4.800000000000001

All the above statements are true if I use a decimal data format instead of IEEE 754 floating-point.

People often use decimal precision when working with currencies, to make sure the above problems don't happen for decimal values that don't round well into binary. But even that's not foolproof:

from decimal import Decimal

a = Decimal(1) / Decimal(2)
b = Decimal(1) / Decimal(3)
c = Decimal(1) / Decimal(4)

# Multiplication is commutative and associative.
In [1]: a * b * c == a * c * b
Out[1]: False

# Multiplication is invertible.
In [2]: Decimal(1) / b * b == Decimal(1)
Out[2]: False

d = Decimal(1) / Decimal(12)
e = Decimal(1) / Decimal(13)
f = Decimal(1) / Decimal(14)

# Addition is commutative and associative.
In [3]: d + e + f == d + f + e
Out[3]: False

# Addition is invertible.
In [4]: Decimal(1) + b - b == Decimal(1)
Out[4]: False

# Multiplication is distributive over addition.
In [5]: 3 * (e + f) == 3 * e + 3 * f
Out[5]: False

# Repeatedly adding a number `n` times will get me the same number as multiplying that number by `n`.
In [6]: sum(Decimal(1) / Decimal(3) for i in range(10)) == Decimal(1) / Decimal(3) * Decimal(10)
Out[6]: False

# Repeatedly adding a number `n` times will get me on the same side of the inequality as multiplying that number by `n`.
In [7]: sum(Decimal(1) / Decimal(38) for i in range(15))
Out[7]: Decimal('0.3947368421052631578947368424')

In [8]: Decimal(1) / Decimal(38) * Decimal(15)
Out[8]: Decimal('0.3947368421052631578947368420')

In [9]: Decimal(15) / Decimal(38)
Out[9]: Decimal('0.3947368421052631578947368421')

All the above statements are true if I use an equality function that allows for a margin of error.

Yes, but then equality is no longer transitive, which means that comparisons of numbers to each other are no longer reliable.

class RoundedNumber:
  def __init__(self, n):
    self.n = n
  def __eq__(a, b):
    EPSILON = 0.00000001
    return -EPSILON < (a.n - b.n) < EPSILON

a = RoundedNumber(8.00000000)
b = RoundedNumber(8.0000000075)
c = RoundedNumber(8.000000015)

In [1]: a == b
Out[1]: True

In [2]: b == c
Out[2]: True

In [3]: a == c
Out[3]: False

All the above statements are true if I use integers only.

Not all languages have a dedicated integer class. In Javascript, for example, all integers are just floating-point numbers where the mantissa is all zeros past the place that the "decimal point" would be if marked by the exponent.

Multiplying two integers together will always give me the exact correct answer.

In node.js:

> 14142135623 * 14142135624
199999999993467730000

The exact answer (from Python) is 199999999993467733752. Javascript truncated the last four digits (3752).

Adding two integers together will always give me the exact correct answer.

In node.js:

> 20000000000000000 + 1
20000000000000000

Numbers will always equal what I assign them.

> c = 199999999999999996;
20000000000000000

Numbers always have exactly one decimal representation.

This one isn't a programmer misconception so much as a general misconception about numbers - I introduce to you 0.9 repeating, which, of course, equals 1.

Also, 0 and -0 are the same number.

Numbers always have at least one decimal representation.

Infinity and -Infinity would like a word with you, in languages where they exist.

Fractional numbers always exist.

In languages where integers are a thing, one thing that will always catch new learners off guard is that division is truncated. For example, in Python 2:

>>> 3 / 2
    1
>>> 7 / 3
    2

Integer division is always truncated.

For people who are used to languages with integers, the language suddenly changing to accommodate accurate division can be jarring. When moving from Python 2 to Python 3, division went from being an integer operation to being possibly-integers, possibly-floating point. Now, if you want integer division, you need to use the double slash (//).

In [1]: 2 / 3
Out[1]: 0.6666666666666666

In [2]: 2 // 3
Out[2]: 0

Negative numbers always exist.

If your integer type is unsigned, subtracting a larger number from a smaller number gives a result that's greater than either of those numbers.

unsigned int a = 1;
unsigned int b = 2;
if (a - b < 0) {
	std::cout << "My assumption was correct!"
} else if (a - b > 0) {
	std::cout << "Phooey."
} else {
	std::cout << "Wait, what?"
}

Phooey.

This will normally give you a compiler warning, but it's still something you should watch out for.

Subtracting 1 modulo n does the same thing as adding n-1.

If your subtraction takes you below 0, the modulus operator will return a negative number, which is bad when you're using it to loop through menu options.

Integers are stored in memory in the order I read them (big-endian format).

On most Intel/x86 systems, integers are stored in little-endian format, which means that the small bytes come first. A number like 0x12345678 would be stored in memory as 78 56 34 12. The order we're used to reading is called the big-endian format.

Integers are stored in little-endian format.

Not all computer systems are Intel/x86. Systems like the IBM Z architecture use big-endian number storage.

Integers are stored in a format with fixed endianness.

Some exotic systems use little-endian for 8-bit bytes within 16-bit words and then big-endian for 16-bit words within 32-bit words. This would turn 0x12345678 into 34 12 78 56.

Rounded percentages always add up to 100%.

If you round your percentages naïvely and then implement a hack to ensure that they add up to 100%, you get errors like this one: https://twitter.com/AWild_Abra/status/966690878965284864

Rounded percentages always add up to 100%, given enough precision.

The example in the post above uses the algorithm "Round all entries except the last one whichever way they go, and subtract the last one from 100%", which will always result in the last entry being given -0.000...001% no matter how precise you make the decimals.

Numbers always have commas as thousands separators.

In different parts of the world, thousands separators can be periods, spaces, or even apostrophes.

72,385,164 people
1 073 741 824 bytes
28.259.274 euros
500'428'694 sq km

The decimal point is always a period.

In almost all of Europe, the comma is used for the decimal marker.

28.259.274,38 euros
1 224 795,128 mètres

Numbers are always separated by the thousands.

In the Chinese and Japanese numeral systems, numbers are grouped in 4 digits, using the 万/億/兆 system.

2527兆1438億2518万9984円
382兆3267亿1093万4200元

Numbers are always separated by equal groups of digits.

In the Indian lakh-crore system, only the last three digits are written in the same group. After that, it's every two digits.

3,45,52,78,194 rupees

Numbers are always grouped the same way when written out in words versus when written out in numerals.

Despite the Chinese and Japanese numeral systems using groups of 4, they tend to use groups of 3 when writing out numbers in Arabic numerals, just like in English.

2527兆1438億2518万9984円 = ¥2,527,143,825,189,984
382兆3267亿1093万4200元 = ¥382,326,710,934,200

Numbers are always grouped when there are too many digits to fit in a single group.

In standard Canadian usage, four-digit numbers are not grouped (e.g. 5291) but five-digit numbers are (e.g. 15 873). Also, year numbers, despite being 4 digits, are never digit-grouped - the current year is 2018, after all.

When abbreviating numbers, the same prefix/suffix always refers to the same number.

Most people are familiar with the controversy over "kilobyte" meaning 1,000 or 1,024 bytes, and the definitions of "megabyte", "gigabyte", and "terabyte" being extrapolated accordingly. Despite the fact that SI standardized on the 1,000 definition, some people still use 1,024 for convenience whenever a power of 2 is involved.

But did you know there are people who use "T" to refer to "thousand" instead of "trillion" in monetary figures (e.g. $235T meaning $235,000 rather than $235,000,000,000,000)? It tripped me up the first time I saw it too.

Also, even when you do use T as trillion, it can mean either 10^12 or 10^18 depending on common usage where you live. Because...

When writing numbers out in words, the same word always refers to the same number.

In most English-speaking countries, a billion refers to 10^9. But in older British usage and European countries, a billion is actually 10^12, while 10^9 is a milliard.

Similarly, in the Chinese counting system, a 兆 can be either 10^12 or 10^16 depending on which counting system you're using.

The same number always has the same prefix and suffix.

Depending on your usage, you can either be using the SI system or the colloquial numerical abbreviation system.

Number SI suffix Colloquial suffix (short count) Colloquial suffix (long count)
47,200 47.2k 47.2K 47.2K
47,200,000 47.2M 47.2M 47.2M
47,200,000,000 47.2G 47.2B 47.2Md or 47,200M
47.2 x 10^12 47.2T 47.2T 47.2B
47.2 x 10^15 47.2P 47.2Q/Qa 47.2Bd or 47,200B
47.2 x 10^18 47.2E 47.2Qt/Qi 47.2T
47.2 x 10^21 47.2Z 47.2Sx 47.2Td or 47,200T
47.2 x 10^24 47.2Y 47.2Sp 47.2Q/Qa

Numbers are always written in base ten.

How often we forget that hexadecimal is a thing.

Numbers are always written in base 2, 8, 10, or 16.

How often we forget that base64 is a thing, but also, the Maya and Aztec used base 20, the Babylonians used base 60, and there is an example of a tribe in Papua New Guinea that uses base 27.

Place values are always written starting with 0, 1, 2, etc.

In binary data encoding schemes like base32, base64, or ASCII85, the character representing zero is A, not 0. The character 0 actually maps to a place value of 52.

Numbers are always written with one character per digit.

Sometimes digits are separated by colons instead. Base 256 (a sequence of bytes) is often expressed with groups of two hexadecimal digits, like in RSA fingerprints (10:e4:7c:92:fd:65:ed:d5:3e:54:65:b8:c7:a7:38:2e). Numbers in arbitrary bases above 36 tend to be written as colon-separated decimal numbers (e.g. 112:48:108:121:110:111:109:49:64:108:115:46:83:69 in base 200).

Numbers are always written with Arabic numerals.

For example, the Eastern Arabic numberals (٠١٢٣٤٥٦٧٨٩) and Thai numerals (๐๑๒๓๔๕๖๗๘๙), among many more, including the Mayan and Babylonian numerals in different bases.

Numbers are always written with numerals.

Not only do Roman numerals exist, but languages like Greek and Hebrew historically used a gematria to express numbers, in which certain letters stood for certain values but with no repeated symbols and way more variety in the range of values that could be taken.

Numbers always exist.

For most of human history, we didn't have precise words or symbols for numbers - only words for vague quantities like "one" or "many".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment