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.

Per-key presence overrides total The total setting sets the default presence for every key, while Required and NotRequired override that default for individual keys. total=True (default) keys required unless overridden order_id: int → required customer: str → required coupon: NotRequired[str] → optional total=False keys optional unless overridden theme: str → optional locale: str → optional user_id: Required[int] → required
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.

Runtime vs static analysis 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 NotRequired key without narrowing. Accessing payload["coupon"] directly assumes it is present; pyright flags this with reportTypedDictNotRequiredAccess. Guard with if "coupon" in payload: or use payload.get("coupon").
  • Wrapping a value in both Required and NotRequired. 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 runtime KeyErrors into type-check-time failures.
  • Assuming total=False makes a key nullable. total=False controls presence, not value; a present key still must match its declared type. Use Required[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.

Back to Literal and TypedDict