# Floating Point and Money

> Why 0.1 plus 0.2 is not 0.3, what floats can and cannot represent, and the iron rule: never store money in a float.


---

# Floating Point and Money

You typed `0.1 + 0.2` into a console once, expecting `0.3`, and got back `0.30000000000000004`. For a second you wondered if the computer was broken, or if you were. Neither. You'd run straight into how computers store fractions - and it's a wall everyone hits, usually right when real money is on the line and a total is off by a cent.

This guide makes that surprise stop being spooky. You'll see *why* it happens (it's not a bug, it's a design), where it quietly bites - money, equality checks, long running totals - and the small set of fixes that put it to rest for good. By the end you'll have one rule burned in that will save you a production incident: **never store money in a float.**

## How to read this
- **Want the one idea?** Read [Phase 1](01-why-floats-surprise-you.md). The whole guide rests on it: a float is a *binary* fraction, so most decimals can't be stored exactly.
- **Want it to stick?** Read in order. We start with why the surprise happens, then where it actually hurts, then exactly what to do instead - and when floats are completely fine.

## The phases
1. **[Why Floats Surprise You](01-why-floats-surprise-you.md)** - the mental model: a float stores numbers in binary, so values like 0.1 round on the way in. The 0.1 + 0.2 demo, decoded.
2. **[Where It Bites](02-where-it-bites.md)** - the three places rounding error turns into real bugs: money, exact equality checks, and long summations. Seen up close.
3. **[The Fixes (and When Floats Are Fine)](03-the-fixes.md)** - integer cents, a decimal type, comparing with a tolerance - and the other half of the story: where floats are exactly the right tool.


---

# Why Floats Surprise You

Let's start at the exact moment it gets weird. You open any language with a console and type the most innocent sum imaginable:

```console
>>> 0.1 + 0.2
0.30000000000000004
```
*What just happened:* The computer added two numbers you'd bet your lunch were `0.1` and `0.2`, and the answer came out wrong in the fifteenth decimal place. Your instinct says "rounding error," and you're right - but the rounding didn't happen during the *addition*. It happened the moment `0.1` and `0.2` were stored, before the plus sign ever ran.

That distinction is the whole guide. So let's see why storing `0.1` is the problem.

## The one idea: a float is a *binary* fraction

You write numbers in base 10. After the decimal point, each place is a tenth, a hundredth, a thousandth - powers of ten. A computer doesn't have base 10; it has base 2. After the (binary) point, each place is a half, a quarter, an eighth, a sixteenth - powers of *two*.

So a float can only store numbers it can build by adding up halves and quarters and eighths. Some decimals fall right out of that:

```text
0.5   = 1/2              → exact in binary
0.25  = 1/4              → exact in binary
0.75  = 1/2 + 1/4        → exact in binary
```
*What just happened:* These decimals are sums of powers of two, so the computer stores them perfectly. No rounding, no surprise. The trouble starts with the ones that *aren't*.

Try to build `0.1` from halves, quarters, eighths, sixteenths… you never land on it exactly. It's like trying to write `1/3` in base 10: `0.3333…` going on forever. In binary, `0.1` is a *repeating* fraction that never terminates:

```text
0.1 in binary = 0.0001100110011001100110011...  (the 0011 repeats forever)
```
*What just happened:* `0.1` has no exact binary form, the same way `1/3` has no exact decimal form. The computer can't store infinite digits, so it keeps about 15–17 significant decimal digits' worth and **rounds off the rest.** What you store as `0.1` is really `0.1` plus a microscopic error.

💡 **Key point.** The error isn't randomness or a CPU flaw. It's the unavoidable cost of writing a base-10 number in a base-2 box. Most decimals don't fit, so they get rounded to the nearest value the box *can* hold.

## Replaying the surprise, now that it makes sense

Go back to `0.1 + 0.2`. Both numbers got rounded slightly when stored. Add two slightly-off numbers and the errors combine, landing you past `0.3`:

```text
stored 0.1  ≈ 0.1000000000000000055511151231257827...
stored 0.2  ≈ 0.2000000000000000111022302462515654...
their sum   ≈ 0.3000000000000000444089209850062616...
nearest 0.3 ≈ 0.2999999999999999888977697537403718...
```
*What just happened:* The true sum of the two stored values is a hair *above* `0.3`, and it's closer to the float just above `0.3` than to the one just below it. So the result displays as `0.30000000000000004`. Nothing went wrong - every step did exactly what it promised. The inputs were never quite `0.1` and `0.2` to begin with.

Here's a cleaner way to *see* that the stored values aren't what you typed. Ask for more digits than the console normally shows:

```python runnable
print(f"{0.1:.17f}")
print(f"{0.2:.17f}")
print(f"{0.1 + 0.2:.17f}")
print(0.1 + 0.2 == 0.3)
```
*What just happened:* Printed to 17 decimals, `0.1` reveals itself as `0.10000000000000001` and the sum as `0.30000000000000004`. And the equality check prints `False` - because the stored sum and the stored `0.3` are two different nearby floats. The default short display had been politely rounding the lie away for you.

## Why it's built this way (and why that's okay)

It would be fair to ask: if base 10 is what humans use, why not store numbers in base 10? The answer is speed and range. The format almost every language uses for `float`/`double` is **IEEE 754** - a binary layout your CPU has dedicated hardware to add, multiply, and divide blindingly fast, and one that covers an enormous range, from subatomic to astronomical, in a fixed 64 bits.

That's a fantastic trade for measuring, simulating, and rendering - places where being off in the 16th digit is invisible and irrelevant. It's a *terrible* trade the moment "off by a hair" means "off by a cent." Which is exactly where we're headed next.

📝 **Terminology.** A *float* (short for *floating-point number*) stores a number as a binary fraction with limited precision. The common 64-bit kind is a *double*. *IEEE 754* is the standard that defines how those bits are laid out. The leftover difference between the number you wanted and the one actually stored is *rounding error.*

> 💬 For why this hardware-friendly trade-off exists at all - and why CPUs care so much about fixed-size, fast number formats - see [How a Computer Actually Works](/guides/cpu-ram-and-storage). And if the base-2 / base-10 fraction idea felt like math you'd rather avoid, [Why Math Isn't Your Enemy](/guides/why-math-isnt-your-enemy) is a gentler on-ramp.

```quiz
[
  {
    "q": "Why does 0.1 + 0.2 produce 0.30000000000000004?",
    "choices": [
      "The CPU has a hardware bug in addition",
      "0.1 and 0.2 can't be stored exactly in binary, so they're rounded before the addition even happens",
      "The plus operator rounds its result up by default",
      "0.3 is a special number that floats can't represent"
    ],
    "answer": 1,
    "explain": "0.1 and 0.2 are repeating fractions in binary, so each is rounded slightly when stored. The addition is exact on those rounded inputs; the inputs were the problem."
  },
  {
    "q": "Which of these decimals CAN be stored exactly as a binary float?",
    "choices": ["0.1", "0.2", "0.75", "0.3"],
    "answer": 2,
    "explain": "0.75 = 1/2 + 1/4, a sum of powers of two, so it's exact. 0.1, 0.2, and 0.3 are repeating binary fractions and get rounded."
  },
  {
    "q": "What is IEEE 754?",
    "choices": [
      "A law requiring decimal money storage",
      "The standard binary layout most languages use for float and double",
      "A rounding mode you can turn off",
      "A base-10 number format built into CPUs"
    ],
    "answer": 1,
    "explain": "IEEE 754 defines how floating-point numbers are stored in bits. It's binary and hardware-friendly, which is exactly why decimal values like 0.1 don't fit."
  }
]
```


---

# Where It Bites

A tiny error in the 16th decimal sounds harmless, and most of the time it is. The danger isn't the size of one error - it's the three situations where that error gets *noticed*: displaying money, checking two numbers for equality, and adding up many numbers. Recognizing them on sight is half the job.

## Bite #1: money, off by a cent

This is the one that ends up in a bug ticket. Picture a checkout adding up an order:

```python runnable
price = 0.10
quantity = 3
total = price * quantity
print(total)
print(f"${total:.2f}")
```
*What just happened:* `0.10 * 3` should be `0.30`, but the stored result is `0.30000000000000004`. Format it to two decimals for display and it *looks* fine - `$0.30`. That's the trap: the display hides the error, so you ship it. The rounding is still in the underlying number, waiting.

It surfaces the moment you do anything *exact* with that total - comparing it, summing many of them, or rounding at a different step. A classic: split a bill and the pennies don't add back up.

```python runnable
bill = 0.30
each = round(bill / 3, 2)   # three people split it
print(each)                 # 0.1 each
print(each * 3)             # do the three shares rebuild the bill?
print(each * 3 == bill)
```
*What just happened:* Each share rounds to `0.1`, but `0.1 * 3` is `0.30000000000000004`, which is *not* equal to the stored `0.30`. In a real ledger this is the missing penny that makes an accountant's reconciliation fail at 2 a.m. The money was never "lost" - it was never represented exactly in the first place.

> ⚠️ A float total that *displays* correctly is the most dangerous kind. The error is invisible until something downstream - a comparison, a sum, a different rounding step - drags it into the light. By then it's in production.

## Bite #2: comparing floats with ==

Because stored floats are approximations, two calculations that *should* give the same value often give two slightly different nearby floats. Checking them with `==` then fails for no visible reason:

```python runnable
a = 0.1 + 0.2
b = 0.3
print(a)            # 0.30000000000000004
print(b)            # 0.3
print(a == b)       # surprise
```
*What just happened:* `a` and `b` are mathematically equal but stored as two different floats, so `==` reports `False`. This is why an exact equality check on floats is a quiet bug magnet: a loop that stops only when a counter "equals" a target, a test asserting `result == 0.3`, a cache keyed on a computed float - each can fail or loop forever even though the math is right.

The fix (next phase) is to stop asking "are these *exactly* equal?" and start asking "are these *close enough*?" But first, the sneakiest bite of the three.

## Bite #3: long summations drift

One rounding error is tiny. Add a million of them and they stop being tiny. When you sum a long list of floats, each addition rounds a little, and the errors *accumulate* in the same direction:

```python runnable
total = 0.0
for _ in range(1_000_000):
    total += 0.1
print(total)              # expected 100000.0
print(total - 100000.0)  # the drift
```
*What just happened:* Adding `0.1` a million times should give `100000.0`, but the result drifts off by a small amount because each step's rounding error piles on the last. The longer the loop or the larger the spread of magnitudes, the worse the drift. This is why scientific and financial code that sums many values has to be careful about *how* it sums them - not whether floats are "accurate enough" in isolation.

💡 **Key point.** The errors aren't getting bigger individually - they're *adding up*. A bound that's invisible on one operation becomes visible over millions. Order and count start to matter.

## The pattern under all three

Notice the common thread. The error from Phase 1 is always there; these three situations are where it becomes *observable*:

```text
money       → you need an EXACT decimal value, and floats hold approximations
equality    → you ask "exactly equal?" of two approximations
summation   → you repeat the rounding enough times that it adds up
```
*What just happened:* Every float bug traces back to the same root - approximation - surfacing through exactness, exact comparison, or accumulation. Once you can name which of the three you're in, the fix in the next phase is obvious.

📝 **Terminology.** *Accumulated error* (or *error accumulation*) is the buildup of many small rounding errors over a sequence of operations. *Exact equality* is comparing with `==` and demanding the bits match - the thing you almost never want with floats.

> 💬 For builders: when you're reviewing code, treat three things as instant red flags - a `float`/`double` holding a currency amount, an `==` between two computed floating-point values, and a long-running sum of floats. None is *guaranteed* wrong, but each deserves a second look and usually one of the fixes coming up next.

```quiz
[
  {
    "q": "A cart total displays correctly as \"$0.30\" but the stored value is 0.30000000000000004. Why is this still dangerous?",
    "choices": [
      "It will display wrong on other screens",
      "The hidden error surfaces in exact comparisons, sums, or later rounding steps",
      "It uses more memory than 0.30",
      "It will round up to $0.31 automatically"
    ],
    "answer": 1,
    "explain": "Formatting for display hides the error but doesn't remove it. Any exact operation downstream - comparison, summation, re-rounding - drags it back into view."
  },
  {
    "q": "Why does 0.1 + 0.2 == 0.3 return False?",
    "choices": [
      "0.3 is stored as a string, not a number",
      "== always returns False for floats",
      "The two sides are stored as two slightly different nearby floats, so an exact match fails",
      "Python rounds the left side up"
    ],
    "answer": 2,
    "explain": "Both sides are approximations stored as different floats. == demands an exact bit match, which they don't have, even though they're mathematically equal."
  },
  {
    "q": "Adding 0.1 a million times drifts away from 100000.0. What's the underlying cause?",
    "choices": [
      "Integer overflow in the loop counter",
      "Each addition rounds slightly, and those errors accumulate over many steps",
      "0.1 changes value during the loop",
      "The CPU gets slower as the number grows"
    ],
    "answer": 1,
    "explain": "Each step introduces a tiny rounding error in the same direction. One is invisible; a million of them accumulate into a visible drift."
  }
]
```


---

# The Fixes (and When Floats Are Fine)

Here's the good news: you don't need to understand IEEE 754 any deeper to be safe. You need a small kit of three habits, matched to the three bites from the last phase - money gets an exact representation, comparisons get a tolerance, and, the part people forget, you let floats keep doing the jobs they're great at.

## Fix #1: store money as integer minor units (cents)

The cleanest money fix is to stop storing fractions at all. A dollar is 100 cents; a price isn't `19.99` dollars, it's `1999` cents. Integers are stored *exactly* - no binary-fraction problem, because there's no fraction. You do all the math in whole cents and only format with a decimal point when you show it to a human.

```python runnable
price_cents = 1999          # $19.99, stored as a whole number
quantity = 3
total_cents = price_cents * quantity
print(total_cents)                          # 5997 - exact, always
print(f"${total_cents // 100}.{total_cents % 100:02d}")
```
*What just happened:* Every value and every operation stays a whole number, so there is no rounding error to accumulate - `1999 * 3` is exactly `5997`. The decimal point only appears at the very end, for display. This is what most payment systems do under the hood, and it's the default you should reach for.

The catch to know about: division. Splitting `5997` cents three ways is `1999` each with `0` left over - clean. But `100` cents split three ways is `33, 33, 33` with `1` cent left over, and *someone* has to get that extra cent. Integer cents doesn't make that decision for you; it makes the leftover **visible and exact** so you can assign it on purpose instead of losing it to rounding.

## Fix #2: use a decimal type when you need fractions of a cent

Sometimes whole cents aren't enough - tax rates, interest, currencies with more than two decimal places, per-unit prices like fuel. For those, most languages offer a **decimal** type that stores numbers in base 10, exactly, the way you write them. It's slower than a float, but it doesn't round `0.1`.

```python runnable
from decimal import Decimal

a = Decimal("0.1") + Decimal("0.2")
print(a)              # 0.3 - exactly
print(a == Decimal("0.3"))
```
*What just happened:* `Decimal` stores `0.1`, `0.2`, and `0.3` as exact base-10 values, so the sum is exactly `0.3` and the equality check is `True`. The catch you must respect: build decimals from **strings** (`Decimal("0.1")`), not from floats - `Decimal(0.1)` would copy the float's rounding error straight in. Most languages have an equivalent: `BigDecimal`, `decimal`, `NUMERIC`/`DECIMAL` in SQL.

> ⚠️ A decimal type only helps if you keep floats out of it. `Decimal("0.1")` is exact; `Decimal(0.1)` inherits the float error and defeats the entire point. Same rule in databases: store money in a `DECIMAL`/`NUMERIC` column, never `FLOAT`/`REAL`.

## Fix #3: compare floats with a tolerance, not ==

When you genuinely are working with floats (you'll see good reasons in a moment), stop asking "exactly equal?" Ask "close enough?" - within a small tolerance that absorbs rounding noise.

```python runnable
a = 0.1 + 0.2
b = 0.3
print(a == b)                       # False - exact comparison
print(abs(a - b) < 1e-9)            # True - within tolerance
import math
print(math.isclose(a, b))           # True - the built-in way
```
*What just happened:* `abs(a - b) < 1e-9` checks whether the two values differ by less than a tiny threshold, which they don't - so it treats them as equal. Better still, many languages ship a ready-made `isclose` that handles the tricky scaling for big and small numbers. The rule: **never `==` two computed floats; compare the difference against a tolerance** (often called an *epsilon*).

📝 **Terminology.** *Minor units* are the smallest whole unit of a currency (cents for USD). A *decimal type* stores numbers in base 10 exactly. *Tolerance* / *epsilon* is the small allowed difference when comparing floats for "close enough."

## The other half of the truth: floats are often exactly right

It would be wrong to leave you afraid of floats. They're not a mistake to be avoided - they're the correct tool for a huge class of problems. Reach for floats freely when:

- **The values are measurements, not exact counts.** Temperatures, distances, sensor readings, weights - these are already approximate; a float's tiny error is far smaller than the real-world uncertainty.
- **You're doing graphics, audio, simulation, or geometry.** Pixel positions, 3D coordinates, physics steps, signal samples. Speed matters enormously and a 16th-digit error is invisible on screen or in the ear.
- **You're doing science or statistics.** Averages, models, probabilities - float's range and speed are exactly what you want, and you compare with tolerances anyway.

```text
USE A FLOAT FOR:        DON'T USE A FLOAT FOR:
  measurements            money / currency
  graphics & geometry     exact counts that must reconcile
  physics & simulation    anything compared with ==
  science & statistics    accumulating sums where exactness matters
```
*What just happened:* The dividing line isn't "floats are bad," it's **exactness**. Need an exact decimal value (especially money) or an exact comparison? Use integers or a decimal type. Working with inherently approximate quantities where speed counts? Floats are the right call. The same property that makes them wrong for dollars makes them perfect for pixels.

💡 **Key point.** One rule covers ninety percent of the danger: **never store money in a float.** Use integer minor units, or a decimal type. Everything else here is detail hanging off that rule.

> 💬 For builders: when you pick up a new language, spend five minutes learning its money story before you write any. Which decimal type does it ship? What's the idiomatic `isclose`? What column type does the database use for currency? Knowing those three answers up front is the difference between a clean ledger and a 2 a.m. reconciliation bug. And if the underlying base-2 idea still feels shaky, [Why Math Isn't Your Enemy](/guides/why-math-isnt-your-enemy) and [How a Computer Actually Works](/guides/cpu-ram-and-storage) are worth a pass.

```quiz
[
  {
    "q": "What's the recommended default for storing a USD price like $19.99 in code?",
    "choices": [
      "A float: 19.99",
      "A string: \"19.99\"",
      "An integer number of cents: 1999",
      "A double rounded to two places"
    ],
    "answer": 2,
    "explain": "Integer minor units (cents) are stored exactly - no binary-fraction rounding. You only add the decimal point when formatting for display."
  },
  {
    "q": "When using a decimal type, why must you write Decimal(\"0.1\") instead of Decimal(0.1)?",
    "choices": [
      "Strings are faster than numbers",
      "Decimal(0.1) inherits the float's rounding error before the decimal type ever sees it",
      "Decimal can't accept numbers at all",
      "It's only a style preference with no real effect"
    ],
    "answer": 1,
    "explain": "0.1 as a float is already rounded. Passing that float into Decimal copies the error in. Building from the string \"0.1\" gives the exact value you wrote."
  },
  {
    "q": "Which task is a GOOD fit for floats?",
    "choices": [
      "A bank account balance",
      "3D coordinates in a graphics engine",
      "An invoice total that must reconcile to the cent",
      "A loop counter compared with == to a target"
    ],
    "answer": 1,
    "explain": "Graphics values are inherently approximate and speed-critical, where a 16th-digit error is invisible. Money and exact comparisons need integers or a decimal type."
  }
]
```
