DEV Community

Super Funicular
Super Funicular

Posted on

Python Wrote NaN Into My JSON and Only Python Could Read It Back

I had a small Python script writing one JSON line per camera clip, with a temperature reading next to each file name. Sometimes there was no reading, so the script stored float('nan') as a placeholder. Python read the file back without a complaint. A Node dashboard reading the same file threw on two of the three lines:

# write_log.py: one JSON line per clip, with a temperature reading that may be missing
import json

clips = [
    {"clip": "clip_0001.mp4", "temp_c": 31.5},
    {"clip": "clip_0002.mp4", "temp_c": float("nan")},  # sensor gave no reading
    {"clip": "clip_0003.mp4", "temp_c": float("inf")},  # "no limit" placeholder
]
with open("clips.jsonl", "w") as f:
    for c in clips:
        f.write(json.dumps(c) + "\n")
Enter fullscreen mode Exit fullscreen mode
python3 write_log.py
cat clips.jsonl
Enter fullscreen mode Exit fullscreen mode
{"clip": "clip_0001.mp4", "temp_c": 31.5}
{"clip": "clip_0002.mp4", "temp_c": NaN}
{"clip": "clip_0003.mp4", "temp_c": Infinity}
Enter fullscreen mode Exit fullscreen mode
# read_py.py: the Python side reads its own file back
import json

with open("clips.jsonl") as f:
    for line in f:
        print(json.loads(line))
Enter fullscreen mode Exit fullscreen mode
// read_node.js: a dashboard reads the same file
const fs = require("fs");

for (const line of fs.readFileSync("clips.jsonl", "utf8").trim().split("\n")) {
  try {
    console.log(JSON.parse(line));
  } catch (e) {
    console.log(e.name + ": " + e.message);
  }
}
Enter fullscreen mode Exit fullscreen mode
python3 read_py.py
node read_node.js
Enter fullscreen mode Exit fullscreen mode
{'clip': 'clip_0001.mp4', 'temp_c': 31.5}
{'clip': 'clip_0002.mp4', 'temp_c': nan}
{'clip': 'clip_0003.mp4', 'temp_c': inf}
{ clip: 'clip_0001.mp4', temp_c: 31.5 }
SyntaxError: Unexpected token 'N', ...""temp_c": NaN}" is not valid JSON
SyntaxError: Unexpected token 'I', ...""temp_c": Infinity}" is not valid JSON
Enter fullscreen mode Exit fullscreen mode

The file is not valid JSON. Python wrote NaN and Infinity as bare words, and the JSON grammar has no such values. Python's own reader accepts them, so a test that writes with Python and reads with Python passes, and the problem shows up only when another program opens the file.

Disclosure: I make an Android camera app. Nothing below uses its code or describes how it stores anything. The scripts here are a desktop Python and Node writing and reading a hand-made file, and every output is pasted from the scripts shown. Versions:

Python 3.10.12
v22.23.4
Enter fullscreen mode Exit fullscreen mode

What the Python docs say about this

This is documented, not a bug. The json module's page describes the allow_nan parameter of dump and dumps like this: "If True (the default), their JavaScript equivalents (NaN, Infinity, -Infinity) are used."

The same page is direct about the result. Under its section on infinite and NaN values, the example comment reads "Neither of these calls raises an exception, but the results are not valid JSON". The page also says the module "does not comply with the RFC in a strict fashion, implementing some extensions that are valid JavaScript but not valid JSON."

So the default favours a round trip inside Python over a file that any JSON parser can read. If Python is the sole reader, you may not notice. If the reader is a browser, a dashboard, JSON.parse in a serverless function, or a strict parser in another language, the line is rejected.

The docs call these "JavaScript equivalents", but JavaScript's own JSON writer does not produce them. Asked to write the same values, it writes null:

// js_writes.js: what the JavaScript side writes for the same values
console.log(JSON.stringify({ clip: "clip_0002.mp4", temp_c: NaN }));
console.log(JSON.stringify({ clip: "clip_0003.mp4", temp_c: Infinity }));
Enter fullscreen mode Exit fullscreen mode
{"clip":"clip_0002.mp4","temp_c":null}
{"clip":"clip_0003.mp4","temp_c":null}
Enter fullscreen mode Exit fullscreen mode

So the two sides do not agree. Python writes a token that JavaScript's parser rejects, and JavaScript, given the same value, writes something different that Python reads as None.

Make the writer fail instead

allow_nan=False turns the silent write into an error at the moment the bad value is produced:

# strict_write.py: same records, allow_nan=False
import json

for c in [{"clip": "clip_0001.mp4", "temp_c": 31.5},
          {"clip": "clip_0002.mp4", "temp_c": float("nan")}]:
    try:
        print(json.dumps(c, allow_nan=False))
    except ValueError as e:
        print("ValueError:", e)
Enter fullscreen mode Exit fullscreen mode
{"clip": "clip_0001.mp4", "temp_c": 31.5}
ValueError: Out of range float values are not JSON compliant
Enter fullscreen mode Exit fullscreen mode

The first record is written and the second raises ValueError. That is usually where you want the failure: in the script that produced the value, with the record still in memory, not later in a dashboard that cannot say where the value came from.

The check is not limited to top-level fields. A value nested inside a list inside an object is caught too:

# nested.py: allow_nan=False also covers values nested deep in the object
import json

summary = {"day": "2026-10-26", "stats": {"clips": 3, "temps": [31.5, float("nan")]}}
try:
    json.dumps(summary, allow_nan=False)
except ValueError as e:
    print("ValueError:", e)
Enter fullscreen mode Exit fullscreen mode
ValueError: Out of range float values are not JSON compliant
Enter fullscreen mode Exit fullscreen mode

Note what the message leaves out. It does not name the key or the position of the bad value. On a large object you will need to find it yourself, so it helps to clean values before they reach dumps rather than relying on the error alone.

Decide what a missing reading should be

Raising an error tells you there is a problem. It does not decide what the file should contain. For a reading that is missing, null is the value JSON has for the absence of a value, and it is also what JavaScript's writer produced above. One way to get there is to replace any float that is not finite before writing:

# clean_write.py: write a missing or out-of-range reading as null
import json, math

def clean(v):
    return None if isinstance(v, float) and not math.isfinite(v) else v

for c in [{"clip": "clip_0001.mp4", "temp_c": 31.5},
          {"clip": "clip_0002.mp4", "temp_c": float("nan")},
          {"clip": "clip_0003.mp4", "temp_c": float("inf")}]:
    row = {k: clean(v) for k, v in c.items()}
    print(json.dumps(row, allow_nan=False))
Enter fullscreen mode Exit fullscreen mode
{"clip": "clip_0001.mp4", "temp_c": 31.5}
{"clip": "clip_0002.mp4", "temp_c": null}
{"clip": "clip_0003.mp4", "temp_c": null}
Enter fullscreen mode Exit fullscreen mode

Every line is now valid JSON, and allow_nan=False stays on as a guard in case a value slips past the cleaning step.

Two things to decide deliberately rather than by default:

  • null loses the difference between NaN and infinity. In the original records, one value meant the sensor gave nothing and the other was a placeholder for no limit. After cleaning they are both null. If a reader needs to tell them apart, write a separate field, such as a status string, instead of overloading one number.
  • This version only cleans top-level values. The clean function above does not walk into nested lists or objects. That is why allow_nan=False stays on: if a nested value is not finite, the write fails loudly instead of producing a bad line.

Make the Python reader as strict as the dashboard

The other half of the problem is that Python's reader accepted the bad file, which is why the bug got past a Python-only test. The docs describe the parse_constant hook on loads as a function called with "'-Infinity', 'Infinity', or 'NaN'" that "can be used to raise an exception if invalid JSON numbers are encountered." Using it in a test makes Python reject what the Node reader rejected:

# strict_read.py: make the Python reader refuse what the dashboard would refuse
import json

def reject(name):
    raise ValueError("non-standard JSON constant: " + name)

with open("clips.jsonl") as f:
    for line in f:
        try:
            print(json.loads(line, parse_constant=reject))
        except ValueError as e:
            print("ValueError:", e)
Enter fullscreen mode Exit fullscreen mode
{'clip': 'clip_0001.mp4', 'temp_c': 31.5}
ValueError: non-standard JSON constant: NaN
ValueError: non-standard JSON constant: Infinity
Enter fullscreen mode Exit fullscreen mode

The output matches the Node run line for line: the first record loads, the other two fail. A test that reads files this way will catch a writer that has gone back to the default.

Checklist

  • Pass allow_nan=False to every json.dump and json.dumps call whose output another program will read.
  • Decide what a missing or out-of-range number should be in the file, and write that on purpose, usually null plus a status field if the reason matters.
  • Clean nested values too, or keep allow_nan=False on so that nested ones fail loudly.
  • In tests, read with parse_constant set to a function that raises, so Python is at least as strict as the other readers.
  • If a file has already been written with the default, it is not valid JSON. Fix it at the writer; changing every reader to accept NaN spreads the problem.

Things I did not test

  • Newer Python. I ran 3.10. The quotes come from the current online documentation, which covers newer releases. The behaviour matched what I saw, and the default has been the same for a long time, but I did not run a newer interpreter, and the exact error message text may differ.
  • Browsers. The JavaScript runs were in Node. Browser engines implement the same JSON.parse and JSON.stringify rules, but I did not run these lines in a browser, and their error messages are worded differently.
  • Other parsers and tools. Command-line tools and other languages' JSON libraries vary in whether they accept these tokens. I did not test any of them.
  • NumPy and pandas. Their own export functions have separate options for missing values. Nothing here covers them.

Sources. Quotes are from the Python documentation page json — JSON encoder and decoder. Outputs are pasted unedited from the scripts above.

If you want an old Android phone recording in the background so there are clips for a script like this to log, the app I make is Background Camera RemoteStream on Google Play. Nothing above depends on it.

Python's json module will write a file that only Python can read unless you tell it not to, so tell it not to.

Top comments (0)