open your terminal. pick any language. doesn't matter which one.
# python
>>> 0.1 + 0.2
0.30000000000000004// javascript
> 0.1 + 0.2
0.30000000000000004// go
fmt.Println(0.1 + 0.2) // 0.30000000000000004same result. python, javascript, go, java, c, c++, rust — all of them. literally every mainstream language on every computer you've ever touched gives you that wrong answer.
and here is the thing most people get wrong about this: it is not a bug. it is not a quirk. it is not the language being lazy. it is a deliberate engineering decision the entire industry made in 1985 and has been quietly living with ever since. you've been living with it too — every time you've written if price == 0.1.
i kept running into this in random places. small rounding errors in a billing script. a unit test that passed locally but failed once. a chess eval display showing +0.30000000000000004 instead of +0.3. eventually i stopped patching the symptom and asked the actual question: what is the computer literally doing when i type 0.1? this is what i found, all the way down.
first, why binary can't store 0.1
computers don't think in base 10. they think in base 2. 1s and 0s. and the moment you switch number systems, some fractions that look perfectly clean in one system turn into infinite repeating decimals in the other.
you've already seen this in base 10 with 1/3:
1/3 = 0.33333333...
it just keeps going. you can never write down 1/3 exactly in decimal. you can only approximate it. write 10 digits, write 100 digits, you're still wrong, just less wrong.
0.1 in binary has the exact same problem. it just doesn't look like it from the outside.
here is the conversion. to turn a decimal fraction into binary, you keep multiplying by 2 and writing down whether you crossed 1 or not:
0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1 (subtract 1, carry 0.6)
0.6 × 2 = 1.2 → 1 (subtract 1, carry 0.2)
0.2 × 2 = 0.4 → 0 ← we are back at 0.2. it loops from here.
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1
...
so 0.1 in binary is 0.00011001100110011001100... — the four bits 0011 repeating forever. 0.2 is the same pattern shifted by one place. neither one terminates. neither one fits in any finite amount of memory.
any fraction whose denominator has a prime factor other than 2 cannot be represented exactly in binary. 10 = 2 × 5. that factor of 5 is the entire reason
0.1is broken.
your computer has finite memory. it cannot store an infinite number of bits. so it cuts the sequence off somewhere and rounds. the question is: where exactly does it cut, and how does that rounding sneak out and ruin your day?
that is where IEEE 754 comes in.
IEEE 754 — what a float actually looks like in memory
IEEE 754 is the standard that decides exactly how a floating-point number is laid out in binary. every CPU since the mid-80s implements it. when you write 0.1 in any language, this is the shape of what gets stored.
a 64-bit double — the default float in basically every language you'll touch — looks like this:
┌─┬───────────┬──────────────────────────────────────────────────┐
│S│ exponent │ mantissa │
│1│ 11 bits │ 52 bits │
└─┴───────────┴──────────────────────────────────────────────────┘
63 62 52 51 0
three fields. each one has a job.
sign bit (1 bit). literally just 0 for positive, 1 for negative. that's it. easiest part of the whole thing.
exponent (11 bits). this is the scale of the number — same idea as scientific notation. when you write 1.5 × 10^3, the 3 is the exponent. floats do exactly that, but in base 2. the exponent tells you how far to shift the decimal point.
11 bits can store values from 0 to 2047. but the actual exponent it represents is that number minus 1023. that subtraction (called the bias) is what lets one field cover both very small numbers (negative exponents) and very large ones (positive exponents) without needing a separate sign bit. so if the exponent bits store 1019, the real exponent is 1019 - 1023 = -4.
mantissa (52 bits). this is where the actual digits of the number live. IEEE 754 always normalizes a number into the form 1.something × 2^exponent. since the leading 1 is always there, you don't bother storing it — you just assume it. that gets you 52 bits of explicit precision plus 1 bit you got for free, so 53 bits of effective precision.
put it all together and the value of any float is:
value = (-1)^sign × 1.mantissa × 2^(exponent - 1023)
now let's run 0.1 through this and watch where the lie shows up.
the exact crime — what 0.1 looks like in the register
we already know 0.1 in binary is 0.00011001100110011... repeating. step one of IEEE 754 is to normalize it into 1.something × 2^n. shift the binary point to the right four places and you get:
0.0001100110011... = 1.100110011001100... × 2^(-4)
so for stored 0.1:
- sign bit:
0(positive) - exponent:
-4 + 1023 = 1019→ stored as01111111011 - mantissa: the 52 bits after the implicit leading
1, which is1001100110011001100110011001100110011001100110011010
look closely at that mantissa. the pattern 1001 was supposed to repeat forever. but we only have 52 bits. so the CPU truncates and rounds the 52nd bit. that round-off is the entire problem. that single bit is why your billing script is wrong.
the actual number sitting in your CPU's register when you type 0.1:
stored 0.1 = 0.1000000000000000055511151231257827021181583404541015625
not 0.1. close. but not 0.1.
0.2 has the same problem:
stored 0.2 = 0.200000000000000011102230246251565404236316680908203125
so when you ask the CPU to add them, here's what actually happens. the CPU is not adding 0.1 + 0.2. it has never seen 0.1 or 0.2 in its life. it is adding two stored values that are almost 0.1 and 0.2:
0.1000000000000000055...
+ 0.2000000000000000111...
= 0.3000000000000000444...
and python (or javascript, or whatever) prints that as 0.30000000000000004. not because the language is being weird. because that is the actual number sitting in the register.
the error doesn't happen during the addition. it already happened the moment you typed
0.1. the addition just makes it visible.
i think this is the part most people miss. they assume floats do exact math on slightly wrong inputs. nope. floats do exact math on the wrong inputs and sometimes round again on the way out. there are two separate places things can go sideways.
why every language gives the same answer
python, javascript, go, java, c++ — different teams, different runtimes, different design philosophies. so why do they all agree on the wrong answer?
because for floating-point math, none of them are doing the math. they all hand the operation off to the same place: the CPU's floating-point unit. that's a chunk of dedicated silicon on your processor that implements IEEE 754 in hardware. it's stupidly fast and it is exactly the same on intel, AMD, apple silicon, every ARM chip in your phone.
so when your python interpreter sees 0.1 + 0.2, it eventually emits a single machine instruction that says "FPU, add these two doubles." the FPU does its IEEE 754 thing and hands back 0.3000000000000000444.... the language never even gets to make a decision. it is a passenger.
# python
>>> 0.1 + 0.2 == 0.3
False// go
fmt.Println(0.1 + 0.2 == 0.3) // false// javascript
0.1 + 0.2 === 0.3; // false// java
System.out.println(0.1 + 0.2 == 0.3); // falsesame silicon. same answer. always.
this is also why "switching languages" has never fixed a floating-point bug for anyone. you weren't running into a python problem. you were running into an ALU problem.
when this stopped being academic
this isn't a quirk you can wave off. it has actually killed people.
the patriot missile, 1991
during the gulf war, the US army deployed Patriot missile defense batteries to intercept iraqi Scud missiles. the system tracked time using a 24-bit fixed-point counter that incremented every 0.1 seconds.
0.1. the same 0.1 we just spent half this post discussing. the system stored a slightly wrong value for "one tenth of a second", and every tick of the counter accumulated that error.
after about 100 hours of continuous operation, the accumulated time error was around 0.34 seconds. a Scud travels at roughly 1676 m/s. multiply that out: 0.34 × 1676 ≈ 570 meters. the radar was looking for the missile in the wrong patch of sky.
on february 25, 1991, a Scud hit a US army barracks in Dhahran, Saudi Arabia. 28 soldiers died. the Patriot battery in the area didn't intercept it. the official US Army investigation traced the failure back to that floating-point time drift.
a number being slightly wrong in a register killed 28 people.
the vancouver stock exchange, 1982
less dramatic, but a beautiful example of how this stuff sneaks up on you. the Vancouver Stock Exchange started a new index at exactly 1000.000 in january 1982. by november that same year, the index was reading around 524. trading had actualy been up the whole time. the index should have been near 1098.
the cause: every time a transaction updated the index, the value was truncated (chopped off, not rounded) to three decimals. each truncation lost a tiny fraction. tens of thousands of transactions a day, every day, for 22 months.
the accumulated error cut the index almost in half. they had to halt the index, recalculate it from scratch, and reset.
floating-point bugs rarely fail loudly. the dangerous ones accumulate in silence for months before anyone notices.
if you are writing anything that loops on a float — physics ticks, billing cycles, sensor sampling, animation timestamps — assume you have one of these bugs and you just haven't seen it yet.
what you should actually use
before i sound like i'm dunking on IEEE 754 — i'm not. it is a beautiful piece of engineering. it lets your CPU do scientific simulation in nanoseconds with 64 bits per number. it powers every game engine, every ML training run, every shader. the tradeoff (15-16 significant decimal digits of precision in exchange for speed) is the right tradeoff for graphics, physics, neural nets, and most science.
the tradeoff is not right for money. or for any place where exactness matters more than throughput. so here is the toolbox.
1. fixed-point — store money as integers
simplest possible fix. never store ₹10.50 as a float. store it as 1050 paisa. all arithmetic happens on integers, which are always exact. you only convert to rupees when you display.
// WRONG — float, will rot eventually
price := 10.50 // float64
// RIGHT — integer paisa, always exact
priceInPaisa := int64(1050) // ₹10.50razorpay, stripe, every serious payment processor does this internally. their core ledgers store cents/paisa/satoshi as integers. floats never touch the money path.
2. decimal types — base-10 numbers
some languages and databases give you a Decimal type that stores numbers in base 10 instead of base 2. since 0.1 is a clean fraction in base 10, the repeating-binary problem just disappears.
# python
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") == Decimal("0.3") # True-- postgres: NUMERIC / DECIMAL is exact base-10 arithmetic
SELECT 0.1::NUMERIC + 0.2::NUMERIC = 0.3::NUMERIC; -- truethe tradeoff: decimal math is slower, because the CPU's FPU only knows IEEE 754. decimal arithmetic is simulated in software. for almost any financial app, the correctness is worth the cost. nobody cares if your invoice generator takes 2ms instead of 0.1ms.
3. epsilon comparison — for when you're stuck with floats
somtimes you inherit a codebase that's already drowning in floats and you can't realistically rip them out. fine. but never compare floats with ==. always check that they are close enough:
// WRONG
if a == b { ... }
// RIGHT
const epsilon = 1e-9
if math.Abs(a - b) < epsilon { ... }picking the right epsilon is its own can of worms — it depends on the magnitude of the numbers and the domain. but == on floats is essentialy a coin flip, so anything is better.
the rule
NEVER USE FLOATS FOR MONEY. NEVER COMPARE FLOATS WITH
==. these are not guidelines. they are load-bearing facts of how computers work.
IEEE 754 is genuinely incredible. it lets a single chip simulate planets, train models, and render shaders, all in the same 64 bits. it powers the entire numerical world.
it was never designed to store your account balance.
so the next time someone on your team writes if price == 0.1, you'll know exactly what's about to go wrong, and now you'll know why — all the way down to which bits got rounded at position 52 inside the FPU.
that is the real answer. not "floating point is imprecise." the actual representation of 0.1 in IEEE 754 double precision is 0.1000000000000000055511151231257827... and it has been that number on every computer you have ever touched in your entire life.
the math was never wrong. the assumption was.