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 к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
