runtime
140 / 250
lesson 140 Data Models & Typing · advanced

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.

You'll be able to
  • 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
The idea

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.

example-1.py · An order payload from JSON
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.

example-2.py · Notifiers that never mention the protocol
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)
example-3.py · A Protocol is not a class to instantiate
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).

Review
  • 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.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

TypedDict and Protocol — Data Models & Typing · Runtime