NaN превратился в None и сломал PydanticНедавно наткнулись на очень показательный баг в Python-проекте с распределёнными процессами.
Один алгоритм считает числовые пороги (`float`) и передаёт их другому процессу в виде JSON. Модели общие, всё на Pydantic — красота, типизация, валидация.
И вдруг второй процесс падает с ошибкой:
>
Input should be a valid number, input_value=NoneХотя первый процесс отправлял… float.
🧩 Что произошло
В одном из порогов оказалось значение NaN:
thr = Thresholds(high=float('nan'), low=0.0)
json_str = thr.model_dump_json()
print(json_str)
# {"high": null, "low": 0.0}
🧩 Почему
NaN стал nullПотому что:
— В IEEE 754 у float есть спецзначение
NaN— В JSON стандарте — нет представления для NaN
— Разрешены только: число, строка, объект, массив,
true, false, nullПоэтому сериализатор делает единственное, что может — превращает
NaN → null.А дальше:
JSON null → Python None
И Pydantic получает:
high=None
Но поле объявлено как
float, а не float | None → валидация падает.То есть:
float('nan') → JSON null → Python None
Сериализация и десериализация не являются обратными операциями.
🧩 Почему это особенно коварно
NaN — валидный float. Его часто используют как “значение не определено”.И у него особая логика:
nan > 10 # False
nan < 10 # False
nan == 10 # False
А вот
None — это уже другой тип, и сравнения просто падают с TypeError. Семантика теряется.🧩 Как это чинить
1. Можно сказать Pydantic сериализовать
NaN как литерал:
{"high": NaN}
model_config = ConfigDict(ser_json_inf_nan='constants')
Работает идеально внутри Python.
Но многие парсеры (JS, БД, API) такой JSON не примут, и формально это уже не RFC-совместимый JSON.
2. Сделать поле
float | None
high: float | None
JSON становится валидным. Но теперь теряется типовая строгость и нужно везде помнить, что
None на самом деле значит NaN.3. Кастомный тип (самый аккуратный)
Сказать модели явно:
>
null в JSON — это NaN, а не None
FloatNaN = Annotated[
float,
PlainSerializer(lambda v: None if math.isnan(v) else v, when_used="json"),
BeforeValidator(lambda v: float("nan") if v is None else v),
]
Теперь:
NaN → null → NaN
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека питониста
#буст