Numbers, precision and NaN

json · Advanced · 10 min

Silent precision loss in prices, IDs and measurements is one of the most expensive JSON bugs.

JSON has a single number type with unlimited digits; the spec leaves precision to implementations. Python decodes numbers without a fraction or exponent as int (arbitrary size, exact) and the rest as float (64-bit binary, about 15 to 17 significant digits).

Consequences:

  • Decimal fractions are approximated: 0.1 + 0.2 is not 0.3, and "19.90" decodes to 19.9. For money, decode with parse_float=Decimal and encode Decimals as strings (or as numbers with a custom approach) so no binary rounding happens.
  • Python's large ints are exact, but JavaScript numbers are floats, so integers above 2**53 (for example 64-bit database IDs or Twitter-style snowflakes) lose precision in browsers. APIs often send such IDs as strings for that reason.
  • 1.0 round-trips as 1.0, not 1, because repr(float) is preserved.

NaN and Infinity: Python writes float('nan') as NaN and infinity as Infinity. These are JavaScript literals, not valid JSON, and strict parsers reject them. Use allow_nan=False to raise ValueError instead, and convert such values to null yourself. On decoding, Python accepts them by default; parse_constant lets you reject them.

1e400 overflows to inf when parsed, so overflow quietly becomes Infinity too.

Example

import json
from decimal import Decimal

print(json.loads("[0.1, 19.90, 1.0]"))   # [0.1, 19.9, 1.0]
prices = json.loads('{"a": 0.1, "b": 0.2}', parse_float=Decimal)
print(prices["a"] + prices["b"])         # 0.3
print(json.loads("[0.1]")[0] + 0.2)      # 0.30000000000000004

big_id = 2**63 - 1
print(json.loads(json.dumps(big_id)) == big_id)   # True in Python
print(float(big_id) == big_id + 1)                # True: a JS float can't tell

print(json.dumps([float("nan"), float("inf")]))   # [NaN, Infinity]
try:
    json.dumps(float("nan"), allow_nan=False)
except ValueError as e:
    print(e)          # Out of range float values are not JSON compliant: nan

Remember

  • JSON numbers become exact ints or binary floats; use parse_float=Decimal for money
  • Integers above 2**53 lose precision in JavaScript clients; send big IDs as strings
  • allow_nan=False stops Python from emitting invalid NaN/Infinity

Watch out for

  • Sending 64-bit IDs as JSON numbers to a browser and getting the last digits rounded.

Check yourself

  1. A Python API returns {"id": 9007199254740993} (2**53 + 1). What does a JavaScript client typically see?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/json.html#json.dumps