0.1 + 0.2 ≠ 0.3 is not a language bug. It happens because IEEE 754 binary floating-point cannot represent most decimal fractions exactly in a finite number of bits. The “floating-point error” people mention starts right here—in the limits of the representation itself.

The first time I hit this was on a web backend that summed cart totals with float. Logs showed 0.30000000000000004, and QA filed a “one cent off” ticket. Code review came back with one line: use epsilon instead of ==. Nobody could explain why 0.1 was already wrong before the addition. That was when it clicked: this is not a printf formatting issue. If you do not know the rules for storing reals in binary, you keep falling into the same trap.

The fastest way to see how 0.1 is stored at the bit level is the IEEE 754 Converter. Enter 0.1, 0.2, and 0.3, then switch between f64 (double) and f32 (single) and compare the sign, exponent, and mantissa fields.

The Real Question Behind a “Broken” Test

In a Python REPL, 0.1 + 0.2 == 0.3 is False. The JavaScript console agrees. The same operation in C++ double does not match 0.3 at the bit level either. New hires often hear “floats are approximate” and stop there; in production the questions branch:

  • Billing and finance: why do whole currency units drift?
  • Games and physics: why does collision logic wobble slightly frame to frame?
  • Embedded and sensor fusion: why does a float hex from UART parse to a subtly different value on the PC?

The common thread is thinking in decimal while storing binary floating-point. It feels like a “bug that will never be fixed,” but IEEE 754 has been the deliberate shared standard across hardware, languages, GPUs, and DSPs since 1985. “Fixing” it would mean changing what float arithmetic means on essentially every CPU.

How IEEE 754 Stores One Number

IEEE 754 (current revision: IEEE Std 754-2019) encodes reals with sign, exponent, and significand. Normalized finite values follow roughly:

[ (-1)^{s} \times 2^{e - bias} \times (1 + f) ]

Here (f) is the fractional part held in the significand field. The crucial point is that the significand is a finite binary fraction. Write (1/10 = 0.1_{10}) in binary and you get a repeating expansion like (0.0001100110011\ldots_{2}), which cannot be cut exactly to 52 bits (f64) or 23 bits (f32). The stored 0.1 is the closest representable value; so are 0.2 and 0.3. Add three independently rounded values and you may not get the same bit pattern as decimal 0.3 computed by hand.

IEEE 754 single-precision 32-bit layout: sign, 8-bit exponent, 23-bit mantissa

How one f32 word splits into sign / exponent / mantissa—the starting point for reading register dumps

Goldberg’s classic "What Every Computer Scientist Should Know About Floating-Point Arithmetic" explains systematically how representation error propagates and blows up through catastrophic cancellation. “0.1 + 0.2” is practically the opening sentence of that textbook.

Reproduce in Code, Then Inspect Bits

Python

a, b, c = 0.1, 0.2, 0.3
print(a + b)          # 0.30000000000000004
print(a + b == c)     # False
print(hex(id(a)))     # for bits, not object id:
import struct
print(struct.unpack("!d", struct.pack("!d", a))[0])
for label, x in [("0.1", a), ("0.2", b), ("0.3", c), ("sum", a+b)]:
    bits = struct.unpack(">Q", struct.pack(">d", x))[0]
    print(f"{label}: {bits:#018x}")
Python

C

#include <stdio.h>
#include <math.h>
#include <stdint.h>

int main(void) {
    double a = 0.1, b = 0.2, c = 0.3;
    double s = a + b;
    printf("%.17g\n", s);
    printf("equal: %d\n", s == c);
    union { double d; uint64_t u; } u = { .d = s };
    printf("bits: 0x%016llX\n", (unsigned long long)u.u);
    return 0;
}
C

C++ (std::numeric_limits and std::bit_cast)

#include <iostream>
#include <iomanip>
#include <limits>
#include <bit>

int main() {
    using limits = std::numeric_limits<double>;
    const double a = 0.1, b = 0.2, c = 0.3;
    const double s = a + b;
    std::cout << std::setprecision(17) << s << "\n";
    std::cout << std::boolalpha << (s == c) << "\n";
    std::cout << "epsilon: " << limits::epsilon() << "\n";
    std::cout << "hex: 0x" << std::hex << std::bit_cast<std::uint64_t>(s) << "\n";
}
C++

Drop to f32 and the error contour gets coarser. In game engines using float coordinates or MCUs running single-precision FFT, QA reports of “snapping to 0.1 increments” often start with f32 quantization.

If firmware gives you a dump like 0x3FB99999A0000000, fix word byte order with the Endian Converter before pasting into the IEEE 754 Converter. Wrong endian interpretation produces a completely different number that looks like a “floating-point bug.” For endian basics, pair this with Understanding Endianness.

Same Math, Different Domains

Web, finance, and billing

JSON and JavaScript Number use IEEE 754 binary64. Money that needs fixed decimal semantics—dollars and cents, won and jeon—should not accumulate in float/double. Use integers in minor units or decimal types (Python Decimal, Java BigDecimal, .NET decimal). Stripe taking amounts in integer cents follows the same pattern.

Games, graphics, and physics

Rendering and physics still lean on float. Logic like “snap to a 0.3 grid” is safer with integer grid coordinates or epsilon comparisons than round(x * 10) / 10. Unity and Unreal run float internally, but editor snap and tile maps often parallel integer coordinate systems.

Embedded and signal processing

When lifting ADC readings to float, a common pattern is normalize scale and offset in integers first, then convert to float at the end. If reals live in CAN or Modbus registers, word order varies by protocol—using the IEEE 754 Converter together with an endian tool saves debug time.

Scientific computing

NumPy float64 is the same standard. numpy.nextafter and np.isclose(atol, rtol) are APIs for the absolute and relative tolerances Goldberg discusses. Make “use isclose, not ==” a habit.

Defensive Patterns in Production

1. Avoid equality; use tolerances

import math
def float_eq(x, y, rel=1e-9, abs_=1e-12):
    return math.isclose(x, y, rel_tol=rel, abs_tol=abs_)
Python

2. Money and counters as integers

Scaling to cents or micro-units in int64 is the safer default.

3. Separate display from storage

Show %.2f in the UI; keep integers or Decimal internally. Logging with %.17g helps teams agree that 0.30000000000000004 is expected behavior.

4. Unit tests: bits or ULP

Numerical libraries often assert against ULP (units in the last place). “Within 1 ULP of c” beats “must equal 0.3” for standards-aligned tests.

Verify with the CompuTools IEEE 754 Converter

The IEEE 754 Converter runs f32 and f64 conversion in the browser. Enter decimal 0.1, 0.2, and 0.3 and compare the hex and binary tabs. When you type the sum of 0.1 + 0.2 directly, you can see visually why its 64-bit pattern differs from 0.3—often starting with one or two mantissa bits.

Special values—NaN, Inf, -0—are classified on the same screen. For sensor faults or post–divide-by-zero results, enter nan, inf, and -0 in the converter first; then when a UART log shows something like 7FF8000000000000, you can recognize it as a special value immediately.

Because computation stays on the client, pasting internal protocol dumps or float strings that touch customer data does not add requests in the network tab—the same local WASM story as in Browser-Based Dev Tools and Local Processing.

References

  1. IEEE Computer Society. IEEE Std 754-2019 – IEEE Standard for Floating-Point Arithmetic — Normative definition of sign, exponent, significand, rounding modes, and special values.
  2. Goldberg, D. "What Every Computer Scientist Should Know About Floating-Point Arithmetic", ACM Computing Surveys, 1991 — Representation error, cancellation, and stable formula choice.
  3. Python Software Foundation. Floating Point Arithmetic: Issues and Limitations — Representation error for 0.1, examples such as 0.1 + 0.1 + 0.1 == 0.3, and the decimal module.
  4. WG14. ISO/IEC 9899:2018 (C17), draft N2310 (PDF) — Annex F: binding of <float.h>, <math.h> to IEC 60559 (IEEE 754).
  5. Wikipedia contributors. Double-precision floating-point format — binary64 layout, bias 1023, and double-precision examples.
Popular Posts
The Mathematical Cause of Gimbal Lock and How to Avoid It: A Complete Guide to Euler Angles vs Quaternions07 March 2026
#Mathematical
Checksum vs CRC vs Hash: Which Should You Use for Data Integrity Verification?15 March 2026
#Security & Hash
Understanding 3D Rotation: A Practical Guide to Quaternions, Euler Angles, and Rotation Matrices21 January 2026
#Mathematical
The Complete Guide to Cron Expressions: Everything a Developer Must Know About Scheduling25 February 2026
#Time & Date
Mastering Robot Arm Kinematics: A Practical Guide to Forward Kinematics, Inverse Kinematics, and DH Parameters24 March 2026
#Engineering