In a TypedDict, how do total=False, Required and NotRequired differ?
answer
- One class knob, two per-key modifiers
- About key presence, not the value
- Class default, overridden per key
- Optional key is not the same as None
- Python 3.11 added the per-key modifiers
basics
~20 stotal=False is a per-class default that makes every key declared in that class body optional. Required and NotRequired are per-key modifiers that override the class default on one key, so a single class can mix both.
solid answer
~40 sBy default every key in a `TypedDict` is required. `class Draft(TypedDict, total=False)` flips the default for that class body, so all of its keys become optional. `Required[T]` and `NotRequired[T]`, added in Python 3.11, work at the level of one key: they state that key's requiredness explicitly regardless of the class default, so you can write `assignee: NotRequired[str]` inside a total class or `id: Required[int]` inside a `total=False` one. All three are about the **presence of the key**, not about the value: a key that may be absent *and* may hold `None` is `NotRequired[str | None]`. Totality also applies only to keys declared in that class body — inherited keys keep whatever requiredness they were declared with.
code
python · 14 linesfrom typing import TypedDict, Required, NotRequired
class Ticket(TypedDict):
id: int
priority: str
assignee: NotRequired[str]
class Draft(TypedDict, total=False):
title: str
body: str
id: Required[int]
print(Ticket(id=1, priority="high"))
print(Draft(id=2))go deeper
Recall that keys in a TypedDict are required unless something says otherwise, and that total=False in the class header is the switch that makes a whole class's keys optional. Know it is checked by a tool, never at run time.
Explain the mechanics precisely: the class-level default, the per-key overrides added in 3.11, and the fact that all three describe key presence rather than value nullability. Be ready to write a class that mixes both.
Show judgment about modelling a wire shape: whether an omitted field and an explicit null mean different things, whether a shape should be read-only through a given function, and why per-key modifiers beat splitting a shape across two classes.
Own the convention. Decide whether shapes in the codebase are described once with per-key modifiers or split by totality, and be clear that none of it constrains data arriving from outside — that requires a validation step you have chosen deliberately.
## The default: everything is required A `TypedDict` declared normally requires every key it lists. Assign a dict literal that omits one and a type checker complains; read a declared key and the checker assumes it is there. ```python from typing import TypedDict class Ticket(TypedDict): id: int priority: str # both keys must be present ``` ## total=False: a per-class default The `total` keyword in the class header flips that default for the keys declared in **that class body**: ```python class Draft(TypedDict, total=False): title: str body: str # both keys may be absent ``` `total=False` does not say the values may be `None`, and it does not make the dict partially typed — it says the keys may be missing. A checker will then refuse to let you read `draft["title"]` unconditionally, because it cannot prove the key is there. ## Required and NotRequired: per-key modifiers Before Python 3.11 the only way to mix required and optional keys in one shape was to split it into two classes — a total base and a `total=False` subclass — and inherit. PEP 655 added `Required[T]` and `NotRequired[T]` in **3.11**, which state requiredness on a single key and win over the class default: ```python from typing import TypedDict, Required, NotRequired class Ticket(TypedDict): # default: required id: int priority: str assignee: NotRequired[str] # ...except this one class Draft(TypedDict, total=False): # default: optional title: str id: Required[int] # ...except this one ``` These are annotations a checker reads; at run time they are ordinary typing objects with no effect. The modern style is to write one class and mark the exceptions, rather than juggling two classes with different totality — it keeps the whole shape readable in one place. ## Presence is not nullability The single most common confusion here is between a key that may be *absent* and a value that may be `None`. They are orthogonal axes: - `assignee: str` — the key is always there, and holds a string. - `assignee: str | None` — the key is always there, and may hold `None`. - `assignee: NotRequired[str]` — the key may be missing; if present it holds a string. - `assignee: NotRequired[str | None]` — the key may be missing, and if present may hold `None`. A wire format that omits a field and one that sends an explicit null are genuinely different, and this is where you encode that difference. ## Totality and inheritance `total` applies only to the keys declared in the class body that carries it. Keys inherited from a base keep the requiredness they were declared with, so a `class Child(Base, total=False)` does **not** relax `Base`'s keys — a frequent wrong answer. Multiple inheritance from several `TypedDict` classes merges their key sets, each key keeping its own requiredness. Once you internalise that totality is per-class rather than per-shape, the per-key modifiers look even more attractive: they say what they mean at the exact place they apply. ## Requiredness is part of the type It helps to remember that these modifiers do not merely annotate a declaration — they are part of what makes two shapes compatible. A shape whose key is required is not interchangeable with one whose key is optional, because code written against the required version is entitled to assume the key is there. So the modifiers propagate: widen a key from required to not-required and every shape that consumed the old one has to cope, which a checker will tell you about at each call site. That is the useful direction of the constraint. It is also why marking a key `NotRequired` is a real change rather than a cosmetic one, and why reaching for `total=False` to quiet a complaint usually makes more work downstream than it saves — you have told every reader of that shape that they must handle absence. ## The third axis: ReadOnly Python 3.13 added `ReadOnly[T]` (PEP 705), a separate modifier again. It does not touch presence — it tells a checker that the key must not be written through this type. That matters for a function that accepts a shape it must not mutate, and it lets a `TypedDict` with a read-only key be compatible with one that declares a narrower value type for it. Like everything else here, it is checked statically only; the object is a plain, fully mutable `dict` at run time and any code can add or reassign keys. ## What none of this does No modifier here is enforced when the program runs. Constructing `Ticket(id=1)` with `priority` missing raises nothing; nor does adding a key that was never declared. Requiredness is a promise your type checker verifies about the code it can see, which is exactly why data crossing a trust boundary needs a real validation step before it is described by these annotations.
- Does NotRequired[str] also allow the value None?No. `NotRequired` is purely about whether the key is present. The value type is a separate axis, so a key that may be missing and may also hold null is `NotRequired[str | None]`, while a key that is always present but nullable is plain `str | None`. Wire formats that distinguish an omitted field from an explicit null depend on exactly this distinction.
- How does totality interact with TypedDict inheritance?`total` applies only to the keys declared in the body that carries it. Inherited keys keep the requiredness they were declared with, so `class Child(Base, total=False)` leaves Base's keys required. That surprise is one reason the per-key `Required` and `NotRequired` modifiers from Python 3.11 are the clearer style: they annotate the key itself rather than a whole class body.
- What does ReadOnly add that total and the per-key modifiers do not?`ReadOnly[T]`, added in Python 3.13, is about writes rather than presence: it tells a type checker that code holding the value through this type must not assign to that key. It also relaxes compatibility, letting a read-only key accept a narrower value type. At run time the value is still a plain mutable dict.
saying these in an interview costs you the question
- Thinks total=False makes values optional rather than keys
- Believes total=False on a subclass relaxes inherited keys
- Confuses NotRequired[str] with Optional[str]
- Says mixing requiredness needs two classes on 3.11+
- Expects a missing required key to raise at construction