← back to the archiveCover illustration for “Reject NaN before exporting evaluation results”
TILday 100·5w ago·Published · Updated ·by Andy Padia

Reject NaN before exporting evaluation results

In short: Reject NaN and infinity with strict JSON serialization before exporting evaluation scores, then test metric validity as a separate requirement.

python ▸ import json

def strict_result(value):
    return json.dumps(value, allow_nan=False)

assert json.loads(strict_result({"score": 0.75, "count": 4})) == {
    "score": 0.75, "count": 4
}
for bad in [float("nan"), float("inf"), -float("inf")]:
    try:
        strict_result({"score": bad})
    except ValueError:
        pass
    else:
        raise AssertionError("non-finite result accepted")

RFC 8259, published in December 2017, excludes NaN and infinity from JSON numbers. Python's JSON encoder can nevertheless emit those tokens with its default settings. A floating-point result can therefore survive your evaluation code and fail at the next system's parser.

Reject NaN before exporting evaluation results. My rule is to make strict serialisation a separate acceptance gate after scoring, even when the object already passed ordinary Python type checks. “It is a float” and “another service can consume it as JSON” are different promises.

Why Python can emit non-JSON numbers

The Python JSON documentation describes this behaviour explicitly. The default encoder accepts non-finite floating-point values, and its decoder accepts the corresponding spellings too. A round trip through that same library can therefore miss an interoperability defect.

The JSON number grammar supplies the boundary: these values are not permitted. This is a format constraint, separate from the question of what an undefined score means to your evaluation.

An empty slice, an overflow or an upstream numeric operation can leave you with a non-finite result. The producer should decide how to represent that state instead of letting a permissive encoder make the decision accidentally.

Reject NaN at the export boundary

This small Python helper rejects non-finite values, including ones nested inside the result object. It does not coerce them to zero or hide the failure.

import json

def strict_result(value):
    return json.dumps(value, allow_nan=False)

assert json.loads(strict_result({"score": 0.75, "count": 4})) == {
    "score": 0.75, "count": 4
}
for bad in [float("nan"), float("inf"), -float("inf")]:
    try:
        strict_result({"score": bad})
    except ValueError:
        pass
    else:
        raise AssertionError("non-finite result accepted")

The finite fixture has two fields so the check exercises an actual result object. The three rejected values cover NaN and both infinity signs. Run the snippet in your supported interpreter as part of the export tests, not only in a notebook where you inspect the printed object.

The synthetic export check

The helper and fixtures were executed with Python 3.14.6. The finite object round-tripped unchanged. Each of the three non-finite inputs raised ValueError. A separate control confirmed that the default encoder emitted the NaN spelling.

Those are local format checks. They do not measure an evaluation system's accuracy, validate a production endpoint or establish that a finite score is meaningful. A perfectly serialisable number can still use the wrong denominator.

Undefined scores need a domain meaning

For a hypothetical retrieval evaluation, I would keep the metric value, eligible-case count and reason for an unavailable result together. If a slice has no eligible cases, an explicit unavailable state is more informative than either zero or a silently emitted NaN.

Do not simply replace every non-finite number with null in a generic cleanup pass. That may be the right wire representation, but only if the schema and downstream calculation agree on its meaning. Otherwise, the chart may quietly drop the case and make the aggregate look better.

There are further boundaries. Strict encoding does not reject every undesirable object shape, enforce numeric ranges or detect duplicate fields in incoming text. It also does not stop an incoming parser from accepting non-standard constants; that is a separate decoder setting and test.

What's in it for you

  • Add NaN and both infinity signs to result-export fixtures.
  • Give undefined metrics an explicit domain meaning before serialising them.

A score is ready to leave Python only when its meaning and its wire format both survive the handoff.

#python#evaluation#testing

Sources

  1. docs.python.org/3.12/library/json.html
  2. rfc-editor.org/rfc/rfc8259.html
← older drop
The Formula E AI coach puts the inference location in focus
newer drop →
Session inventory cannot explain token theft

related drops

explore all 402 drops →
← back to the archiveday 139