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")
python3 write_log.py
cat clips.jsonl
{"clip": "clip_0001.mp4", "temp_c": 31.5}
{"clip": "clip_0002.mp4", "temp_c": NaN}
{"clip": "clip_0003.mp4", "temp_c": Infinity}
# 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))
// 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);
}
}
python3 read_py.py
node read_node.js
{'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
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
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 }));
{"clip":"clip_0002.mp4","temp_c":null}
{"clip":"clip_0003.mp4","temp_c":null}
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)
{"clip": "clip_0001.mp4", "temp_c": 31.5}
ValueError: Out of range float values are not JSON compliant
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)
ValueError: Out of range float values are not JSON compliant
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))
{"clip": "clip_0001.mp4", "temp_c": 31.5}
{"clip": "clip_0002.mp4", "temp_c": null}
{"clip": "clip_0003.mp4", "temp_c": null}
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:
-
nullloses 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 bothnull. 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
cleanfunction above does not walk into nested lists or objects. That is whyallow_nan=Falsestays 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)
{'clip': 'clip_0001.mp4', 'temp_c': 31.5}
ValueError: non-standard JSON constant: NaN
ValueError: non-standard JSON constant: Infinity
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=Falseto everyjson.dumpandjson.dumpscall 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
nullplus a status field if the reason matters. - Clean nested values too, or keep
allow_nan=Falseon so that nested ones fail loudly. - In tests, read with
parse_constantset 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
NaNspreads 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.parseandJSON.stringifyrules, 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)