When would you choose a TypedDict over a dataclass or a NamedTuple for a payload?
answer
- Ask what the value wants to be
- No conversion versus a real runtime type
- Keys and brackets, not attributes
- Structural sameness versus distinct types
- Edge shapes versus domain objects
basics
~20 sChoose a TypedDict when the data is already a dictionary and stays one — decoded payloads, config blobs, kwargs bundles. You get key and value checking with no conversion. Choose a real class when the value carries behaviour or identity.
solid answer
~40 sThe deciding question is whether the value wants to *be* a dictionary. A decoded payload, a config blob or a bundle of keyword arguments is already a `dict` and is often handed straight to something else that expects a mapping; describing it with a `TypedDict` adds key names and per-key types for free, with no conversion and no runtime object. A `dataclass` or a `NamedTuple` gives you something a `TypedDict` cannot: a real runtime type, attribute access, somewhere to hang methods and invariants, and a nominal identity — two `TypedDict` types with the same keys are interchangeable to a checker, whereas two classes are not. The cost is a conversion at the boundary in both directions. A common house rule: typed dictionaries at the edges and for `**kwargs`, real objects for domain values.
code
python · 10 linesfrom typing import TypedDict, Unpack
class RetryOptions(TypedDict, total=False):
attempts: int
backoff: float
def submit(url: str, **options: Unpack[RetryOptions]) -> tuple[str, dict]:
return url, dict(options)
print(submit("/triage", attempts=3, backoff=1.5))go deeper
Recall the surface difference: a TypedDict value is a dict you index with brackets, while a dataclass or NamedTuple value is an object with attributes. Know that only the latter can carry methods.
Explain the tradeoff in both directions — no conversion and no runtime object versus a real type with attributes, behaviour and nominal identity — and name a concrete case for each, such as a keyword-argument bundle versus a domain value.
Demonstrate boundary thinking: which shapes stay dictionaries, where the single conversion happens, and why structural compatibility between identically shaped payloads is a real risk worth designing around.
Own it as a codebase convention rather than a per-file choice: how far edge shapes are allowed to travel inward, what the cost of a conversion layer buys in validation and clarity, and how consistently the team is expected to apply the rule.
## Frame it as one question Asked to compare the three, weak answers list features. A strong answer starts from a single question: **does this value want to be a dictionary, or does it want to be an object?** Everything else follows. ## What a TypedDict is good at Some data is dictionary-shaped by nature and stays that way for its whole life in the program: - a decoded JSON body that will be read, maybe modified, and re-encoded; - a configuration blob; - a bundle of keyword arguments passed through several layers; - a row handed to a library whose API takes a mapping. For all of these, a `TypedDict` gives the shape names and per-key types without changing the value at all. There is no conversion in either direction, no allocation of a new object, and nothing to keep in sync with the wire format. It also composes with `typing.Unpack` (Python 3.12) to type `**kwargs` precisely — a place where a dataclass simply does not fit: ```python def submit(url: str, **options: Unpack[RetryOptions]) -> Response: ... ``` ## What you give up `TypedDict` produces a plain `dict`, so: - **Access is by key**, `t["id"]`, never `t.id`. - **There is nowhere to put behaviour.** A method belongs to a class; a typed dictionary has no class at run time, so logic about the shape lives in free functions. - **There is no nominal identity.** Compatibility is structural, so two `TypedDict` types declaring the same keys and value types are mutually assignable. If two payloads have identical shapes but must not be confused, you need real classes — or a discriminating literal key inside the shape. - **There is nothing to isinstance against**, which removes both runtime guards and the runtime-dispatch patterns that rely on types. - **Nothing is immutable.** Any code holding the value can add or reassign keys. Python 3.13's `ReadOnly` marks individual keys read-only for a type checker, but the object itself stays a fully mutable dict. ## What a dataclass or a NamedTuple buys Both give you a genuine runtime type. That means attribute access, a place for methods and validation in `__post_init__`-style construction, defaults, a distinct type for dispatch and instance checks, and — for a `NamedTuple` — a hashable, iterable, tuple-compatible value that can be a dict key or a set member. They also give you nominal typing: two classes with identical fields remain different types, which is exactly what you want for `Meters` versus `Feet`, or for two similarly shaped events that must never be swapped. The price is a conversion at every boundary. Data arrives as a dictionary and has to be turned into an object, and usually turned back to be sent onward. That conversion is a fine place to validate — but it is code you now own, and for a value that is only being forwarded it is pure overhead. ## What happens when the shape changes The two options also age differently, which is worth raising unprompted. Renaming a key in a `TypedDict` is a rename in one declaration plus every subscript that uses it, and a checker finds them all — but only in code the checker sees, and a dictionary is easy for dynamic code to reach into with a computed key. Renaming a field on a class carries the same benefit and the same limit, with the extra property that the conversion layer gives you one obvious place to keep an old wire name mapped to a new internal name. If the external shape and the internal vocabulary are expected to drift apart, that conversion layer stops being overhead and starts being the point. ## A workable decision rule Use a `TypedDict` when the value is dictionary-shaped data in transit: it is decoded from or encoded to a mapping, it is forwarded to something that wants a mapping, or it is a keyword-argument bundle. Use a `dataclass` when the value is a domain object with behaviour, invariants, defaults, or an identity that must not be confused with a structurally identical neighbour. Use a `NamedTuple` for a small, fixed, immutable record — especially one that must be hashable or unpackable, or that is returned from a function where a tuple is idiomatic. Many real codebases use two of them at once and are right to: a `TypedDict` describing the wire shape at the edge, converted once into domain objects for the interior. That is not indecision — it is the boundary doing its job, and it answers the follow-up an interviewer usually asks next, which is what happens when the payload turns out not to match. Neither choice validates anything on its own; the conversion step is simply a natural place to put the validation you need anyway.
- Two TypedDicts declare identical keys and types. Are they interchangeable?To a type checker, yes — TypedDict compatibility is structural, judged on keys and value types rather than on the class name. If two identically shaped payloads must stay distinguishable you need nominal types, meaning real classes, or a discriminating literal key inside each shape that code can branch on.
- How would you stop callers mutating a shape you passed them?Not at run time — it is a plain dict, and any holder can add or reassign keys. Python 3.13's `ReadOnly[T]` marks individual keys read-only for type checkers, which catches writes in checked code and nothing else. If mutation genuinely must be impossible, that is an argument for an immutable object rather than a typed dictionary.
- Is it reasonable to use both in one service?Yes, and it is a common shape: a TypedDict describes the wire payload at the edge, and a single conversion turns it into domain objects for the interior. The conversion is where validation belongs anyway, so the two choices are not competing — they sit on opposite sides of one boundary.
saying these in an interview costs you the question
- Claims a TypedDict gives attribute access like a dataclass
- Thinks methods can be defined on a TypedDict class
- Believes two identically shaped TypedDicts are distinct types
- Picks a TypedDict for a domain object with behaviour
- Assumes ReadOnly makes the dictionary immutable at run time