Topic 10.5
TypedDict, NamedTuple, Literal and Final
In one line
These tools describe specific shapes: TypedDict labels the keys of a dictionary, NamedTuple gives tuple fields names, Literal allows only listed values, Final marks a constant, and Self, Annotated and @override cover methods that return their own class, extra metadata and overriding.
Think of it like this
A paper form with boxes. A blank sheet lets you write anything anywhere. A form says: name goes here, age goes there, and "title" must be one of Mr, Ms, Dr. Hints can describe the shape of data the same way, so mistakes show up before anyone fills in the form wrongly.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- `TypedDict`
- A dictionary type with named keys and a type for each value.
- `NotRequired`
- Marks a TypedDict key that may be missing.
- `Literal`
- A type that allows only the listed values, like
Literal["a", "b"]. - `NamedTuple`
- A tuple whose items have names and types.
- `Final`
- A label for a name that should never be reassigned.
- `Self`
- A hint meaning "an instance of the same class as this one".
- `Annotated`
- A type plus extra notes that libraries can read; checkers ignore the notes.
- `@override`
- A decorator marking that a method replaces one from a parent class.
- Tag (discriminator)
- A field such as
rolewhose value tells you which shape of data you have.
Step by step
01TypedDict: a shape for a dictionary
Write it like a class with annotated fields. This one is the shape of one chat message. The role is a Literal so only three values are allowed. name is optional.
from typing import Literal, NotRequired, TypedDict
Role = Literal["user", "assistant", "system"]
class Message(TypedDict):
role: Role
content: str
name: NotRequired[str]
msg: Message = {"role": "user", "content": "Hello"}02What the checker catches
A checker rejects {"role": "user"} (missing content), {"rol": ...} (unknown key), and "year": "2009" where an int is expected. Python runs all of them. The run-time example shows that even Message(role="robot", content="x") is accepted.
03NamedTuple: readable small records
Point(3) fills y with its default. You can unpack x, y = p, index p[0], and call p._asdict(). Tuples cannot be changed, so a NamedTuple can be a dictionary key as long as every field is itself hashable (an int or str, not a list).
04Final and final
MAX_TOKENS: Final = 4096 makes the checker object to a later MAX_TOKENS = 1. Python does not. Treat Final as documentation that the build enforces; the capital letters are the Python convention for the same thing.
05Self: methods that return their own class
A builder method returns self so calls can be chained. If it is hinted -> Builder, then after HtmlBuilder().add("a") the checker thinks you have a plain Builder and the .bold() call is an error. -> Self fixes it: the return type follows the subclass.
06@override and Annotated
Put @override above a method that replaces a parent's method. If a refactor renames speak in the parent, the checker flags every child that still says @override def speak. At run time, @override just sets the attribute __override__ = True.
Annotated[int, "years"] is still an int to the checker. The extra items are for tools. get_type_hints(..., include_extras=True) keeps them; without it, they are stripped.
07The AI connection
Every LLM API speaks in dictionaries with a fixed shape: a list of messages with a role and content, tool definitions, tool results. Modelling them as TypedDict with Literal roles gives you checker help for free and is exactly how the official SDKs (the libraries each AI company ships for calling its service) type their requests. For data you must validate at run time, the same shape becomes a Pydantic model (Topic 10.7).
Try it yourself
- 1
Add an optional key
In the first example, add
tool_call_id: NotRequired[str]toMessageand printsorted(Message.__optional_keys__). Both optional keys appear. - 2
Chain a method
In the second example, change
-> Selfto-> Builderonaddand run again. It still works at run time (Python ignores hints), but a checker would now reject.bold()after.add(...). PutSelfback.
Code & diagrams
At run time a TypedDict is a dict, nothing is enforced, and the class remembers which keys are required.
Expected output
{'role': 'user', 'content': 'Hello'} dict
is a plain dict: True
bad role is accepted at run time: {'role': 'robot', 'content': 'x'}
required: ['content', 'role']
optional: ['name']
Options required: ['model']
roles: ('user', 'assistant', 'system')
Point(x=3, y=0) 3 0 {'x': 3, 'y': 0}
unpacked: 3 0
MAX_TOKENS = 4096
MAX_TOKENS now 1@override needs Python 3.12; Self needs 3.11. Annotated extras are visible only when asked for.
Expected output
['a', '<b>', 'b']
Meow | __override__ = True
{'age': typing.Annotated[int, 'years', 0]}
metadata: ('years', 0)
plain: {'age': <class 'int'>}The Movie TypedDict from the checker session of Topic 10.3: a wrong value type is reported with the key name.
mypy more.pyOutput
more.py:31: error: Incompatible types (expression has type "str", TypedDict item "year" has type "int") [typeddict-item]
more.py:45: error: Argument 1 to "label" has incompatible type "Literal['system']"; expected "Literal['user', 'assistant']" [arg-type]A model reply is a list of blocks, each with a Literal type tag. After the checker sees block["type"] == "text" it knows the block is a TextBlock. assert_never makes the checker fail if a third block type is added and render forgets it. At run time isinstance does not work on a TypedDict.
Expected output
Let me check the weather.
[calls weather with {'city': 'Pune'}]
TypeError: TypedDict does not support instance and class checksBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Override a method that does not exist
Mark a method @override but misspell its name.
from typing import override
class Animal:
def speak(self) -> str:
return "..."
class Cat(Animal):
@override
def spek(self) -> str: # typo
return "Meow"
print(Cat().speak())Myth vs fact
Myth
A TypedDict validates its keys when I create it.
Fact
No. At run time it is an ordinary dict. The example creates a message with the role "robot" without a complaint. Use Pydantic when you need run-time validation.
Myth
Final makes a constant that cannot change.
Fact
Only for the checker. Python has no real constants; the assignment MAX_TOKENS = 1 ran fine above.
Myth
NamedTuple and dataclass are the same.
Fact
A NamedTuple is an immutable tuple (it unpacks, indexes, hashes). A dataclass is a normal class with generated methods, and mutable unless you freeze it.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
For API payloads, a tagged union of TypedDicts works well: each shape has a
Literaltypefield ({"type": "text", ...}or{"type": "tool_use", ...}) and the checker narrows to the right shape after you test the tag.matchon the dict (Topic 10.8) uses the same tag. - ▸
RequiredandNotRequiredarrived in 3.11. For older versions the same names live intyping_extensions, a backport package that carries newer typing features to older Pythons. - ▸
Literalvalues must be literals:str,int,bool,bytes,Noneor enum members. For a dynamic set of choices useEnum(Topic 8.9) or aStrEnum. - ▸
@overrideis a typing tool and its run-time effect (__override__ = True) is a stamp, not a check.@typing.finalon a class or method is similar: the checker forbids subclassing or overriding, Python does not. - ▸
A TypedDict **cannot be used with
isinstance**:isinstance(d, Message)raisesTypeError: TypedDict does not support instance and class checks, because the class is erased to a plaindict. To check a shape at run time, write the checks yourself or validate with Pydantic (Topic 10.7). Python 3.13 addedReadOnly[...]for keys the code must not assign (Cfg.__readonly_keys__), and 3.12 added**kwargs: Unpack[Opts]so keyword arguments can be typed by a TypedDict. A TypedDict is also structural: any dict literal with the right keys fits, but a generaldict[str, str]does not fit a TypedDict, and a TypedDict fitsMapping[str, object]but notdict[str, object](writing arbitrary values through adictcould break the shape). - ▸
Selfis also the right return type of alternative constructors:@classmethod def from_dict(cls, data: dict) -> Selfreturns aSubwhen called onSub, so subclasses do not need to re-declare it. Before 3.11 you wrote a boundTypeVarfor the same job.Annotatedis the hinge between the checker and libraries:Annotated[int, Field(ge=0)]is anintto mypy and a validated field to Pydantic, and FastAPI readsAnnotated[int, Query(gt=0)]to build request parsing and the OpenAPI docs.
Remember this
- 1
**
TypedDict** describes a dictionary (Topic 4.4) with known keys, each with its own type. It is how you label data that arrives as dictionaries, such as a chat message or a web API reply, without turning it into a class. You write it like a small class with one field per line: arolethat is astrand acontentthat is astr. - 2
At run time a TypedDict is a plain
dict(check withisinstance(msg, dict)), so nothing is enforced. The checker, though, rejects a missing key, a misspelled key or a wrong value type. - 3
By default every key of a TypedDict is required.
NotRequired[str](Python 3.11) marks one key as optional. Writingtotal=Falseon the class makes every key optional, andRequired[...]then marks the ones that must stay. The class keeps the two sets in__required_keys__and__optional_keys__. - 4
**
Literal** allows only specific values:Literal["user", "assistant", "system"]. This is exactly how chat-message roles or modes such as"fast"and"slow"are described. Because the allowed list is known, a checker can warn about the typo"asistant"and, withassert_never(Topic 10.8), can tell you when you forgot to handle one of them. - 5
**
NamedTuple** is a tuple (Topic 4.3) whose positions have names and types. You writeclass Point(NamedTuple)with fieldsx: intandy: int = 0, one per line. It unpacks like a tuple, indexes like a tuple, and reads like an object (p.x). It cannot be changed after creation (immutable), it is light, and it suits small records. - 6
Dataclasses (Topic 8.8) are the choice when you need your own methods, changeable fields or inheritance.
- 7
**
Final** tells the checker that a name must not be reassigned:MAX_TOKENS: Final = 4096. Python itself still lets you reassign it (there are no real constants in Python); the checker will object. The same module also has@finalto forbid subclassing (building a new class on top of this one, Topic 8.5) or overriding. - 8
Three more helpers.
- 9
**
Self** (3.11) means "the same class as the object that called the method". A method that returnsselfso calls can be chained (a.add().add()) then keeps the exact class. - 10
**
Annotated[int, "years"]** attaches extra notes (metadata) to a type without changing the type. Checkers ignore the notes; libraries read them (Pydantic uses them for rules likeField(ge=0), meaning "greater than or equal to 0"). - 11
**
@override** (3.12) marks a method that is meant to replace a method of the parent class. A decorator is a line starting with@above a function that adds behaviour to it (Topic 9.6). The checker fails if the parent has no such method, which catches typos and renames.
Explain it without notes
What is a TypedDict at run time, and what does it give you at all?
When would you use Literal instead of str?
Why is -> Self better than naming the class when a method returns self?
How does a NamedTuple differ from a plain tuple and from a dataclass?
Can you validate that a dictionary matches a TypedDict at run time with isinstance? What would you do instead?
Practice
Define a TypedDict named ToolCall with name: str and arguments: dict[str, str], create one, and print its __required_keys__ sorted.
Define a NamedTuple Rgb with r, g, b ints and print Rgb(255, 0, 0) and its g value.
Define a type Mode = Literal["fast", "slow"] and print its allowed values using get_args.
Trade-offs
- ↔
TypedDict versus dataclass versus Pydantic. TypedDict labels data you already hold as dicts (JSON) with no conversion. A dataclass gives behaviour. Pydantic validates. Pick by where the data comes from and what you need to do with it.
- ↔
Literal is strict and brittle. Adding a new allowed value touches every place that handles them, which is the point (exhaustive checks) but costs effort.
- ↔
Hints only. None of these tools stops a wrong value while running; they move mistakes from production to your editor.
Done when you can
Done when you can describe a dictionary's shape with a TypedDict, including an optional key.
Done when you can restrict a value with
Literal.Done when you can write a
NamedTupleand say how it differs from a dataclass.Done when you can use
Final,Selfand@overrideand say what they do at run time.Done when you can read extra metadata from
Annotated.