Required and NotRequired Keys in TypedDict
TL;DR — Required[...] and NotRequired[...] (PEP 655) set whether a single key must be present, independent of the TypedDict’s total= setting. In a default total=True dict, mark the one optional key NotRequired[...]; in a total=False dict, mark the one mandatory key Required[...]. That per-key control replaces the old all-or-nothing choice and the awkward split-into-two-classes inheritance trick.
Before PEP 655, a TypedDict had exactly one presence knob: the class-wide total flag. If most keys were required but one was optional, you either split the schema into a required base and a total=False subclass, or gave up precision. Required and NotRequired remove that constraint — presence is now a property of each key. They live in typing from Python 3.11, and in typing_extensions for older runtimes.
total sets each key's default presence; Required and NotRequired flip it for one key at a time.A mostly-required dict with one optional key
Start with a total=True TypedDict — the default — where every key is mandatory except one. Mark that single key NotRequired:
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import NotRequired, TypedDict
class OrderPayload(TypedDict):
order_id: int
customer: str
coupon: NotRequired[str] # may be absent
good: OrderPayload = {"order_id": 7, "customer": "acme"} # coupon omitted → OK
also: OrderPayload = {"order_id": 7, "customer": "acme", "coupon": "SAVE10"}
bad: OrderPayload = {"order_id": 7} # mypy: Missing key "customer" [typeddict-item]
# pyright: reportGeneralTypeIssues
Because the dict is total=True, order_id and customer are required by default and only coupon is optional. This is the shape you reach for constantly: an API record where nearly everything is mandatory and a field or two is optional.
A mostly-optional dict with one required key
Invert it. A settings patch where callers send whatever they want to change is naturally total=False, but you still need one anchoring key present. Mark that key Required:
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import Required, TypedDict
class SettingsPatch(TypedDict, total=False):
theme: str
locale: str
user_id: Required[int] # must always be present
ok: SettingsPatch = {"user_id": 42} # everything else omitted → OK
ok2: SettingsPatch = {"user_id": 42, "theme": "dark"}
nope: SettingsPatch = {"theme": "dark"} # mypy: Missing key "user_id" [typeddict-item]
Required and NotRequired never nest — a key is exactly one or the other — and they cannot wrap each other. What they can wrap is any value type, including a Required[str | None] when the key must be present but its value may be null. That is a genuinely different constraint from the key being absent, which is the subject of the total=False vs Optional guide.
Unknown keys and typo detection
Setting per-key presence also tightens what counts as a valid key. A key that is not declared at all is a different error from a declared-but-missing one:
# Python 3.11+, mypy 1.10 / pyright 1.1.370
patch: SettingsPatch = {"user_id": 1, "themme": "dark"}
# mypy: Extra key "themme" for TypedDict "SettingsPatch" [typeddict-unknown-key]
# pyright: reportGeneralTypeIssues
The [typeddict-unknown-key] code catches typos in literal dict displays. It fires only for keys that are entirely unknown to the schema; a known key that is simply omitted (and required) is [typeddict-item] instead. Keeping the two straight speeds up debugging: unknown-key means “you spelled it wrong or it does not belong,” missing-item means “you left out something mandatory.”
Analyzer behavior
mypy reports a missing required key and a wrong-typed value under [typeddict-item], and an undeclared key under [typeddict-unknown-key]. pyright folds both into reportGeneralTypeIssues, and additionally offers reportTypedDictNotRequiredAccess when you read a NotRequired key without first narrowing its presence. Both checkers understand Required/NotRequired natively on 3.11+; on older interpreters import them from typing_extensions and the same diagnostics apply. None of this needs strict mode, though pyright’s not-required-access report is stricter about reads than mypy’s default. See pyright vs mypy for where the two diverge on optional-key access.
Required and NotRequired are pure type-checker constructs. At runtime a TypedDict is a plain dict, so nothing stops you building {"order_id": 7} without the required customer key — no exception is raised until some later code does payload["customer"] and hits a genuine KeyError. If you need presence enforced when data arrives, validate with Pydantic or an explicit key check at the boundary.
Common mistakes
- Reading a
NotRequiredkey without narrowing. Accessingpayload["coupon"]directly assumes it is present; pyright flags this withreportTypedDictNotRequiredAccess. Guard withif "coupon" in payload:or usepayload.get("coupon"). - Wrapping a value in both
RequiredandNotRequired. A key is one or the other; nesting them is rejected — mypy reports an invalid TypedDict definition. Pick the single correct qualifier. - Typos in a literal dict.
{"themme": ...}is an undeclared key — mypy[typeddict-unknown-key]. This is a feature: it turns silent runtimeKeyErrors into type-check-time failures. - Assuming
total=Falsemakes a key nullable.total=Falsecontrols presence, not value; a present key still must match its declared type. UseRequired[str | None]if the key must exist but may be null.
FAQ
Can I mix Required and NotRequired in the same TypedDict?
Yes. Set the class-wide total to whichever default fits most keys, then override the exceptions individually. A total=True dict can carry several NotRequired keys, and a total=False dict can carry several Required ones.
Do I still need the inheritance trick of a base plus a total=False subclass?
No. Per-key Required/NotRequired was designed to retire that pattern. It remains valid and readable for large schemas, but for a handful of exceptions the per-key qualifiers are clearer and keep the whole shape in one class.