C.W.K.
Stream
Lesson 03 of 05 · published

TypedDict — Structured Dicts with Types

~18 min · typeddict, structured-dict, notrequired

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

The dict shape problem

You have a dict that represents a structured record — {"name": ..., "age": ..., "tags": [...]}. A type hint of dict[str, Any] tells you nothing useful. TypedDict lets you describe the keys and their types so the checker can verify usage.

Two declaration styles

Class-style: class User(TypedDict): name: str; age: int. Or functional: User = TypedDict("User", {"name": str, "age": int}). The class style reads better; the functional style is needed when keys aren't valid Python identifiers (like keys with hyphens). Both produce the same type at runtime.

NotRequired and total=False

By default, every field in a TypedDict is required. total=False in the class declaration makes them all optional. NotRequired[X] (3.11+) marks specific fields optional while keeping the rest required. Required[X] does the inverse — required field in an otherwise total=False dict.

TypedDict at runtime — still a dict

A TypedDict is a dict at runtime. isinstance(user, dict) is True. The type information is purely for static checking. Don't try to call User(name=..., age=...) as a constructor that validates — it just makes a dict.

When to choose TypedDict vs dataclass vs Pydantic

TypedDict when you have JSON-shaped data and want types. Dataclass when you want a Python class with attributes (obj.name not obj["name"]). Pydantic when you also need runtime validation and rich field semantics. Pippa uses Pydantic heavily for FastAPI request/response models.

Code

TypedDict — class style·python
from typing import TypedDict

class User(TypedDict):
    name: str
    age: int
    email: str

def greet(u: User) -> str:
    return f"hi {u['name']} ({u['age']})"

u: User = {"name": "alice", "age": 30, "email": "a@x.com"}
print(greet(u))

# At runtime, u is a regular dict
print(type(u))                         # <class 'dict'>
print(isinstance(u, dict))             # True
Optional fields — NotRequired (3.11+)·python
from typing import TypedDict, NotRequired

class User(TypedDict):
    name: str
    age: int
    nickname: NotRequired[str]            # optional
    bio: NotRequired[str]                 # optional

minimal: User = {"name": "alice", "age": 30}                       # OK
full: User = {"name": "bob", "age": 25, "nickname": "b", "bio": "hi"}

# Pre-3.11 — total=False makes everything optional
class Settings(TypedDict, total=False):
    theme: str
    font: str
    debug: bool

s: Settings = {}                       # OK — all fields optional
s2: Settings = {"theme": "dark"}      # OK
Functional style — for non-identifier keys·python
from typing import TypedDict

# Sometimes API responses have keys that aren't valid Python names
# Functional style handles this
GoogleApiResponse = TypedDict("GoogleApiResponse", {
    "status": str,
    "error-code": int,         # hyphen in key — invalid identifier
    "User-Agent": str,         # capital and hyphen
})

resp: GoogleApiResponse = {
    "status": "ok",
    "error-code": 0,
    "User-Agent": "...",
}
TypedDict vs dataclass — pick the shape·python
from typing import TypedDict
from dataclasses import dataclass

# TypedDict — when you want a dict with checked structure
class UserDict(TypedDict):
    name: str
    age: int

def use_dict(u: UserDict):
    print(u["name"])              # bracket access

# Dataclass — when you want a class with attributes
@dataclass
class UserClass:
    name: str
    age: int

def use_class(u: UserClass):
    print(u.name)                 # attribute access

# JSON in/out — TypedDict is closer to the wire format
# Domain logic — dataclass reads better

External links

Exercise

Define a TypedDict Stock with required fields ticker: str, price: float, shares: int, and NotRequired fields note: str and tags: list[str]. Write a function total_value(s: Stock) -> float that returns price * shares. Build several Stock dicts (some with optional fields, some without) and call the function. Verify mypy is happy by mentally running the type checker through it.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.