TypedDict and Protocol
JSON arrives as dictionaries, and many objects only need to have the right methods. TypedDict describes the keys a dictionary should have; Protocol describes the methods an object should offer, without making it inherit anything.
- Write a TypedDict for a JSON payload, with required and optional (
NotRequired) keys - Use a TypedDict's
__required_keys__to check incoming data at run time - Describe an interface with Protocol and write functions that accept any object with those methods
Data from APIs and JSON files stays as dictionaries. A TypedDict class lists the keys such a dictionary should have and the type of each value, so functions can say def save(order: OrderPayload) instead of a vague dict. Keys are required unless marked NotRequired[...].
A TypedDict is purely a description: at run time the data is still an ordinary dict, and nothing checks it. But the class keeps two useful sets, __required_keys__ and __optional_keys__, which your own code can use to check data where it enters the program.
Run each example to see what it prints. Running examples doesn't count toward progress.
import json from typing import NotRequired, TypedDict class OrderPayload(TypedDict): order_id: str customer_id: str total: float coupon: NotRequired[str] def missing_fields(payload): return sorted(key for key in OrderPayload.__required_keys__ if key not in payload) incoming = json.loads('{"order_id": "O100282", "total": 35.84}') order: OrderPayload = incoming print(type(order).__name__, sorted(OrderPayload.__required_keys__), sorted(OrderPayload.__optional_keys__)) print(missing_fields(incoming)) print(missing_fields({"order_id": "O1", "customer_id": "C1", "total": 9.5, "coupon": "SPRING"}))
In the Inheritance & Composition module several unrelated classes offered send(message), and functions used them interchangeably. A Protocol writes that expectation down: a class that inherits from typing.Protocol and lists the methods, each with only a docstring as its body (you will also see ... used there). Any class with matching methods satisfies it, without inheriting from it. This is called *structural typing*, duck typing that a type checker can verify. A function annotated notifiers: list[Notifier] accepts email, Slack or anything else with send.
from typing import Protocol class Notifier(Protocol): def send(self, message: str) -> str: """Deliver the message and return a receipt.""" class EmailNotifier: def __init__(self, address: str) -> None: self.address = address def send(self, message: str) -> str: return f"email to {self.address}: {message}" class SlackNotifier: def __init__(self, channel: str) -> None: self.channel = channel def send(self, message: str) -> str: return f"slack {self.channel}: {message}" def broadcast(notifiers: list[Notifier], message: str) -> list[str]: return [notifier.send(message) for notifier in notifiers] for receipt in broadcast([EmailNotifier("ops@runtime.market"), SlackNotifier("#orders")], "O100282 delayed"): print(receipt)
from typing import Protocol class Notifier(Protocol): def send(self, message: str) -> str: """Deliver the message and return a receipt.""" print("defined") notifier = Notifier()
When to use it: TypedDict for data that stays as dictionaries (JSON in and out); a dataclass when you convert the data into objects. Protocol for "anything that can do X" parameters, especially when the classes come from different libraries and can't share a parent.
Mistakes to avoid: believing a TypedDict validates incoming JSON (check it yourself, as above); making classes inherit from a Protocol just to satisfy it (unnecessary); and protocols with many methods, which few classes satisfy (keep them small, describing what one function needs).
- TypedDict documents a dictionary's keys and value types; required unless
NotRequired. - At run time a TypedDict value is a plain dict: check
__required_keys__and types yourself where data enters. - Protocol describes the methods an object must have; matching classes satisfy it without inheriting.
- Next module: abstract base classes, runtime protocol checks, mixins and descriptors.