TypedDict и JSON: где действительно проверять входные данные

Функция принимает Order и обращается к order["count"]. В коде тип описан через TypedDict, но после json.loads в count внезапно приходит строка. Аннотация описала договорённость для проверки кода, а вход из файла эту договорённость ещё не прошёл.

Принять внешний объект как неизвестный

json.loads возвращает обычные значения Python. TypedDict не создаёт класс с автоматической валидацией: во время выполнения запись остаётся словарём. Поэтому граница загрузки должна принять object, проверить форму и только затем вернуть Order. После этой функции внутреннему коду уже не приходится повторять те же проверки.

Определите правила явно. В примере нужны непустой артикул и положительное целое количество, лишние поля запрещены. В Python bool является подклассом int, поэтому isinstance(True, int) вернёт True. Для количества выбираем type(count) is int, чтобы логическое значение не стало одной единицей товара. Это следствие конкретной договорённости, а не обязательное правило для всех числовых полей.

Создать новый проверенный словарь

После проверки возвращаем новую структуру только с нужными полями. Исходный объект остаётся у вызывающего кода; изменение его верхнего уровня не меняет возвращённый Order. Вложенные структуры потребовали бы отдельного решения о копировании. Здесь поля состоят из строки и целого числа, поэтому этой границы достаточно.

Проверки примера включают обычный вход, строковое число, bool, пустой артикул и неизвестное поле. Все отклонённые варианты дают ValueError. Код использования считает количество коробок только после успешной загрузки.

import json
from typing import TypedDict

class Order(TypedDict):
    sku: str
    count: int

def parse_order(value: object) -> Order:
    if not isinstance(value, dict) or set(value) != {"sku", "count"}:
        raise ValueError("expected sku and count")
    sku, count = value["sku"], value["count"]
    if not isinstance(sku, str) or not sku.strip():
        raise ValueError("invalid sku")
    if type(count) is not int or count <= 0:
        raise ValueError("invalid count")
    return {"sku": sku, "count": count}

order = parse_order(json.loads('{"sku":"A","count":3}'))
assert order["count"] * 2 == 6
bad_inputs = [
    {"sku": "A", "count": "3"}, {"sku": "A", "count": True},
    {"sku": "", "count": 3}, {"sku": "A", "count": 3, "x": 1},
]
for value in bad_inputs:
    try:
        parse_order(value)
    except ValueError:
        pass
    else:
        raise AssertionError(value)
print("validated order:", order)

Не скрывать проверку за cast

typing.cast сообщает статическому анализатору предполагаемый тип, но не исследует значения. Замена parse_order на cast(Order, json.loads(...)) убрала бы всю проверку внешней границы. Используйте утверждение типа только там, где основание уже доказано другим кодом, и сохраняйте это основание понятным.

Если форматов много, поддержка ручных проверок становится отдельной задачей. Тогда можно выбрать библиотеку валидации и перенести те же правила в её схему. Сначала всё равно нужно решить вопросы лишних полей, преобразования строк в числа и смысла пустых значений: библиотека не выбирает правила предметной области за автора.

Закрепить аннотации на реальной границе

Пройдите открытый урок type hints и mypy, затем добавьте свою функцию загрузки с явной runtime-проверкой. Посмотрите программу: работа с JSON и типизацией помогает соединить внешний формат с внутренней моделью. TypedDict в этой статье служит дополнительным примером.

Продолжить на практике

  • Python для продвинутых: ООП, типизация и тестирование ↗

    Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.